Migrate::synchronizeTable()   D
last analyzed

Complexity

Conditions 18
Paths 64

Size

Total Lines 46
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 342

Importance

Changes 0
Metric Value
cc 18
eloc 31
nc 64
nop 1
dl 0
loc 46
rs 4.8666
c 0
b 0
f 0
ccs 0
cts 31
cp 0
crap 342

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/*
3
 You may not change or alter any portion of this comment or credits
4
 of supporting developers from this source code or any supporting source code
5
 which is considered copyrighted (c) material of the original comment or credit authors.
6
7
 This program is distributed in the hope that it will be useful,
8
 but WITHOUT ANY WARRANTY; without even the implied warranty of
9
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
 */
11
12
namespace Xmf\Database;
13
14
use Xmf\Module\Helper;
15
use Xmf\Yaml;
16
17
/**
18
 * Xmf\Database\Migrate
19
 *
20
 * For a given module, compare the existing tables with a defined target schema
21
 * and build a work queue of DDL/SQL to transform the existing tables to the
22
 * target definitions.
23
 *
24
 * Typically Migrate will be extended by a module specific class that will supply custom
25
 * logic (see preSyncActions() method.)
26
 *
27
 * @category  Xmf\Database\Migrate
28
 * @package   Xmf
29
 * @author    Richard Griffith <[email protected]>
30
 * @copyright 2018 XOOPS Project (https://xoops.org)
31
 * @license   GNU GPL 2 or later (http://www.gnu.org/licenses/gpl-2.0.html)
32
 * @link      https://xoops.org
33
 */
34
class Migrate
35
{
36
37
    /** @var false|\Xmf\Module\Helper|\Xoops\Module\Helper\HelperAbstract  */
38
    protected $helper;
39
40
    /** @var string[] table names used by module */
41
    protected $moduleTables;
42
43
    /** @var Tables */
44
    protected $tableHandler;
45
46
    /** @var string yaml definition file */
47
    protected $tableDefinitionFile;
48
49
    /** @var array target table definitions in Xmf\Database\Tables::dumpTables() format */
50
    protected $targetDefinitions;
51
52
    /**
53
     * Migrate constructor
54
     *
55
     * @param string $dirname module directory name that defines the tables to be migrated
56
     *
57
     * @throws \InvalidArgumentException
58
     * @throws \RuntimeException
59
     */
60 3
    public function __construct($dirname)
61
    {
62 3
        $this->helper = Helper::getHelper($dirname);
0 ignored issues
show
Documentation Bug introduced by
It seems like Xmf\Module\Helper::getHelper($dirname) can also be of type Xoops\Module\Helper. However, the property $helper is declared as type Xmf\Module\Helper|Xoops\...er\HelperAbstract|false. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
63 3
        if (false === $this->helper) {
64
            throw new \InvalidArgumentException("Invalid module $dirname specified");
65
        }
66 3
        $module = $this->helper->getModule();
0 ignored issues
show
Bug introduced by
The method getModule() does not exist on Xoops\Module\Helper. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

66
        /** @scrutinizer ignore-call */ 
67
        $module = $this->helper->getModule();

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...
67 3
        $this->moduleTables = $module->getInfo('tables');
0 ignored issues
show
Documentation Bug introduced by
It seems like $module->getInfo('tables') can also be of type string. However, the property $moduleTables is declared as type string[]. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
68 3
        if (empty($this->moduleTables)) {
69
            throw new \RuntimeException("No tables established in module");
70
        }
71 3
        $version = $module->getInfo('version');
72 3
        $this->tableDefinitionFile = $this->helper->path("sql/{$dirname}_{$version}_migrate.yml");
0 ignored issues
show
Bug introduced by
The method path() does not exist on Xoops\Module\Helper. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

72
        /** @scrutinizer ignore-call */ 
73
        $this->tableDefinitionFile = $this->helper->path("sql/{$dirname}_{$version}_migrate.yml");

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...
73 3
        $this->tableHandler = new Tables();
74 3
    }
75
76
    /**
77
     * Save current table definitions to a file
78
     *
79
     * This is intended for developer use when setting up the migration by using the current database state
80
     *
81
     * @internal intended for module developers only
82
     *
83
     * @return int|false count of bytes written or false on error
84
     */
85
    public function saveCurrentSchema()
86
    {
87
        $this->tableHandler = new Tables(); // start fresh
88
89
        $schema = $this->getCurrentSchema();
90
91
        foreach ($schema as $tableName => $tableData) {
92
            unset($schema[$tableName]['name']);
93
        }
94
95
        return Yaml::save($schema, $this->tableDefinitionFile);
96
    }
97
98
    /**
99
     * get the current definitions
100
     *
101
     * @return array
102
     */
103
    public function getCurrentSchema()
104
    {
105
        foreach ($this->moduleTables as $tableName) {
106
            $this->tableHandler->useTable($tableName);
107
        }
108
109
        return $this->tableHandler->dumpTables();
110
    }
111
112
    /**
113
     * Return the target database condition
114
     *
115
     * @return array|bool table structure or false on error
116
     *
117
     * @throws \RuntimeException
118
     */
119
    public function getTargetDefinitions()
120
    {
121
        if (!isset($this->targetDefinitions)) {
122
            $this->targetDefinitions = Yaml::read($this->tableDefinitionFile);
0 ignored issues
show
Documentation Bug introduced by
It seems like Xmf\Yaml::read($this->tableDefinitionFile) can also be of type boolean. However, the property $targetDefinitions is declared as type array. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
123
            if (null === $this->targetDefinitions) {
124
                throw new \RuntimeException("No schema definition " . $this->tableDefinitionFile);
125
            }
126
        }
127
        return $this->targetDefinitions;
128
    }
129
130
    /**
131
     * Execute synchronization to transform current schema to target
132
     *
133
     * @param bool $force true to force updates even if this is a 'GET' request
134
     *
135
     * @return bool true if no errors, false if errors encountered
136
     */
137
    public function synchronizeSchema($force = true)
138
    {
139
        $this->tableHandler = new Tables(); // start fresh
140
        $this->getSynchronizeDDL();
141
        return $this->tableHandler->executeQueue($force);
142
    }
143
144
    /**
145
     * Compare target and current schema, building work queue in $this->migrate to synchronized
146
     *
147
     * @return string[] array of DDL/SQL statements to transform current to target schema
148
     */
149
    public function getSynchronizeDDL()
150
    {
151
        $this->getTargetDefinitions();
152
        $this->preSyncActions();
153
        foreach ($this->moduleTables as $tableName) {
154
            if ($this->tableHandler->useTable($tableName)) {
155
                $this->synchronizeTable($tableName);
156
            } else {
157
                $this->addMissingTable($tableName);
158
            }
159
        }
160
        return $this->tableHandler->dumpQueue();
161
    }
162
163
    /**
164
     * Perform any upfront actions before synchronizing the schema.
165
     *
166
     * The schema comparison cannot recognize changes such as renamed columns or renamed tables. By overriding
167
     * this method, an implementation can provide the logic to accomplish these types of changes, and leave
168
     * the other details to be handled by synchronizeSchema().
169
     *
170
     * An suitable implementation should be provided by a module by extending Migrate to define any required
171
     * actions.
172
     *
173
     * Some typical uses include:
174
     *  - table and column renames
175
     *  - data conversions
176
     *  - move column data
177
     *
178
     * @return void
179
     */
180
    protected function preSyncActions()
181
    {
182
    }
183
184
    /**
185
     * Add table create DDL to the work queue
186
     *
187
     * @param string $tableName table to add
188
     *
189
     * @return void
190
     */
191
    protected function addMissingTable($tableName)
192
    {
193
        $this->tableHandler->addTable($tableName);
194
        $this->tableHandler->setTableOptions($tableName, $this->targetDefinitions[$tableName]['options']);
195
        foreach ($this->targetDefinitions[$tableName]['columns'] as $column) {
196
            $this->tableHandler->addColumn($tableName, $column['name'], $column['attributes']);
197
        }
198
        foreach ($this->targetDefinitions[$tableName]['keys'] as $key => $keyData) {
199
            if ($key === 'PRIMARY') {
200
                $this->tableHandler->addPrimaryKey($tableName, $keyData['columns']);
201
            } else {
202
                $this->tableHandler->addIndex($key, $tableName, $keyData['columns'], $keyData['unique']);
203
            }
204
        }
205
    }
206
207
    /**
208
     * Build any DDL required to synchronize an existing table to match the target schema
209
     *
210
     * @param string $tableName table to synchronize
211
     *
212
     * @return void
213
     */
214
    protected function synchronizeTable($tableName)
215
    {
216
        foreach ($this->targetDefinitions[$tableName]['columns'] as $column) {
217
            $attributes = $this->tableHandler->getColumnAttributes($tableName, $column['name']);
218
            if ($attributes === false) {
219
                $this->tableHandler->addColumn($tableName, $column['name'], $column['attributes']);
220
            } elseif ($column['attributes'] !== $attributes) {
221
                $this->tableHandler->alterColumn($tableName, $column['name'], $column['attributes']);
222
            }
223
        }
224
225
        $tableDef = $this->tableHandler->dumpTables();
226
        if (isset($tableDef[$tableName])) {
227
            foreach ($tableDef[$tableName]['columns'] as $columnData) {
228
                if (!$this->targetHasColumn($tableName, $columnData['name'])) {
229
                    $this->tableHandler->dropColumn($tableName, $columnData['name']);
230
                }
231
            }
232
        }
233
234
        $existingIndexes = $this->tableHandler->getTableIndexes($tableName);
235
        if (isset($this->targetDefinitions[$tableName]['keys'])) {
236
            foreach ($this->targetDefinitions[$tableName]['keys'] as $key => $keyData) {
237
                if ($key === 'PRIMARY') {
238
                    if (!isset($existingIndexes[$key])) {
239
                        $this->tableHandler->addPrimaryKey($tableName, $keyData['columns']);
240
                    } elseif ($existingIndexes[$key]['columns'] !== $keyData['columns']) {
241
                        $this->tableHandler->dropPrimaryKey($tableName);
242
                        $this->tableHandler->addPrimaryKey($tableName, $keyData['columns']);
243
                    }
244
                } else {
245
                    if (!isset($existingIndexes[$key])) {
246
                        $this->tableHandler->addIndex($key, $tableName, $keyData['columns'], $keyData['unique']);
247
                    } elseif ($existingIndexes[$key]['unique'] !== $keyData['unique']
248
                        || $existingIndexes[$key]['columns'] !== $keyData['columns']
249
                    ) {
250
                        $this->tableHandler->dropIndex($key, $tableName);
251
                        $this->tableHandler->addIndex($key, $tableName, $keyData['columns'], $keyData['unique']);
252
                    }
253
                }
254
            }
255
        }
256
        if (false !== $existingIndexes) {
257
            foreach ($existingIndexes as $key => $keyData) {
258
                if (!isset($this->targetDefinitions[$tableName]['keys'][$key])) {
259
                    $this->tableHandler->dropIndex($key, $tableName);
260
                }
261
            }
262
        }
263
    }
264
265
    /**
266
     * determine if a column on a table exists in the target definitions
267
     *
268
     * @param string $tableName  table containing the column
269
     * @param string $columnName column to check
270
     *
271
     * @return bool true if table and column combination is defined, otherwise false
272
     */
273
    protected function targetHasColumn($tableName, $columnName)
274
    {
275
        if (isset($this->targetDefinitions[$tableName])) {
276
            foreach ($this->targetDefinitions[$tableName]['columns'] as $col) {
277
                if (strcasecmp($col['name'], $columnName) === 0) {
278
                    return true;
279
                }
280
            }
281
        }
282
283
        return false;
284
    }
285
286
    /**
287
     * determine if a table exists in the target definitions
288
     *
289
     * @param string $tableName table containing the column
290
     *
291
     * @return bool true if table is defined, otherwise false
292
     */
293
    protected function targetHasTable($tableName)
294
    {
295
        if (isset($this->targetDefinitions[$tableName])) {
296
            return true;
297
        }
298
        return false;
299
    }
300
301
    /**
302
     * Return message from last error encountered
303
     *
304
     * @return string last error message
305
     */
306 1
    public function getLastError()
307
    {
308 1
        return $this->tableHandler->getLastError();
309
    }
310
311
    /**
312
     * Return code from last error encountered
313
     *
314
     * @return int last error number
315
     */
316 1
    public function getLastErrNo()
317
    {
318 1
        return $this->tableHandler->getLastErrNo();
319
    }
320
}
321