Passed
Push — develop ( 71b08a...99379a )
by Nikolay
06:34
created

UpdateDatabase   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 369
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 53
eloc 191
c 3
b 0
f 0
dl 0
loc 369
rs 6.96

6 Methods

Rating   Name   Duplication   Size   Complexity  
A updateDbStructureByModelsAnnotations() 0 12 2
A __construct() 0 3 1
A updateDatabaseStructure() 0 8 2
B updateIndexes() 0 34 7
B isTableStructureNotEqual() 0 49 9
F createUpdateDbTableByAnnotations() 0 208 32

How to fix   Complexity   

Complex Class

Complex classes like UpdateDatabase 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 UpdateDatabase, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Copyright © MIKO LLC - All Rights Reserved
4
 * Unauthorized copying of this file, via any medium is strictly prohibited
5
 * Proprietary and confidential
6
 * Written by Alexey Portnov, 6 2020
7
 */
8
9
namespace MikoPBX\Core\System\Upgrade;
10
11
use MikoPBX\Common\Models\PbxSettings;
12
use MikoPBX\Core\System\Util;
13
use MikoPBX\Core\Config\RegisterDIServices;
14
use Phalcon\Db\Column;
15
use Phalcon\Db\Index;
16
use Phalcon\Di;
17
use ReflectionClass;
18
use RuntimeException;
19
20
use function MikoPBX\Common\Config\appPath;
21
22
23
class UpdateDatabase extends Di\Injectable
24
{
25
26
    /**
27
     * @var \Phalcon\Config
28
     */
29
    private $config;
30
31
    /**
32
     * UpdateDatabase constructor.
33
     *
34
     */
35
    public function __construct()
36
    {
37
        $this->config = $this->di->getShared('config');
38
    }
39
40
    /**
41
     *
42
     */
43
    public function updateDatabaseStructure(): void
44
    {
45
        try {
46
            RegisterDIServices::recreateDBConnections(); // after storage remount
47
            $this->updateDbStructureByModelsAnnotations();
48
            RegisterDIServices::recreateDBConnections(); // if we change anything in structure
49
        } catch (RuntimeException $e) {
50
            echo "Errors within database upgrade process";
51
        }
52
    }
53
54
    /**
55
     * Обходит файлы с описанием моделей и создает таблицы в базе данных
56
     *
57
     * @return bool
58
     */
59
    private function updateDbStructureByModelsAnnotations(): bool
60
    {
61
        $result    = true;
62
        $modelsDir = appPath('src/Common/Models');
63
        $results   = glob("{$modelsDir}/*.php", GLOB_NOSORT);
64
        foreach ($results as $file) {
65
            $className        = pathinfo($file)['filename'];
66
            $moduleModelClass = "MikoPBX\\Common\\Models\\{$className}";
67
            $this->createUpdateDbTableByAnnotations($moduleModelClass);
68
        }
69
70
        return $result;
71
    }
72
73
    /**
74
     * Create, update DB structure by code description
75
     *
76
     * @param $modelClassName string class name with namespace
77
     *                        i.e. MikoPBX\Common\Models\Extensions or Modules\ModuleSmartIVR\Models\Settings
78
     *
79
     * @return bool
80
     */
81
    public function createUpdateDbTableByAnnotations(string $modelClassName): bool
82
    {
83
        $result = true;
84
        if (
85
            ! class_exists($modelClassName)
86
            || count(get_class_vars($modelClassName)) === 0) {
87
            return true;
88
        }
89
        // Test is abstract
90
        try {
91
            $reflection = new ReflectionClass($modelClassName);
92
            if ($reflection->isAbstract()) {
93
                return true;
94
            }
95
        } catch (\ReflectionException $exception) {
96
            return false;
97
        }
98
        $model                 = new $modelClassName();
99
        $connectionServiceName = $model->getReadConnectionService();
100
        if (empty($connectionServiceName)) {
101
            return false;
102
        }
103
104
        $connectionService = $this->di->getShared($connectionServiceName);
105
        $metaData          = $this->di->get('modelsMetadata');
106
107
        //https://docs.phalcon.io/4.0/ru-ru/annotations
108
        $modelAnnotation = $this->di->get('annotations')->get($model);
109
110
        $tableName       = $model->getSource();
111
        $table_structure = [];
112
        $indexes         = [];
113
114
        // Create columns list by code annotations
115
        $newColNames       = $metaData->getAttributes($model);
116
        $previousAttribute = '';
117
        foreach ($newColNames as $attribute) {
118
            $table_structure[$attribute] = [
119
                'type'      => Column::TYPE_VARCHAR,
120
                'after'     => $previousAttribute,
121
                'notNull'   => false,
122
                'isNumeric' => false,
123
                'primary'   => false,
124
            ];
125
            $previousAttribute           = $attribute;
126
        }
127
128
        // Set data types
129
        $propertiesAnnotations = $modelAnnotation->getPropertiesAnnotations();
130
        if ($propertiesAnnotations !== false) {
131
            $attributeTypes = $metaData->getDataTypes($model);
132
            foreach ($attributeTypes as $attribute => $type) {
133
                $table_structure[$attribute]['type'] = $type;
134
                // Try to find size of field
135
                if (array_key_exists($attribute, $propertiesAnnotations)) {
136
                    $propertyDescription = $propertiesAnnotations[$attribute];
137
                    if ($propertyDescription->has('Column')
138
                        && $propertyDescription->get('Column')->hasArgument('length')
139
                    ) {
140
                        $table_structure[$attribute]['size'] = $propertyDescription->get('Column')->getArgument(
141
                            'length'
142
                        );
143
                    }
144
                }
145
            }
146
        }
147
148
        // For each numeric column change type
149
        $numericAttributes = $metaData->getDataTypesNumeric($model);
150
        foreach ($numericAttributes as $attribute => $value) {
151
            $table_structure[$attribute]['type']      = Column::TYPE_INTEGER;
152
            $table_structure[$attribute]['isNumeric'] = true;
153
        }
154
155
        // For each not nullable column change type
156
        $notNull = $metaData->getNotNullAttributes($model);
157
        foreach ($notNull as $attribute) {
158
            $table_structure[$attribute]['notNull'] = true;
159
        }
160
161
        // Set default values for initial save, later it fill at Models\ModelBase\beforeValidationOnCreate
162
        $defaultValues = $metaData->getDefaultValues($model);
163
        foreach ($defaultValues as $key => $value) {
164
            if ($value !== null) {
165
                $table_structure[$key]['default'] = $value;
166
            }
167
        }
168
169
        // Set primary keys
170
        // $primaryKeys = $metaData->getPrimaryKeyAttributes($model);
171
        // foreach ($primaryKeys as $attribute) {
172
        //     $indexes[$attribute] = new Index($attribute, [$attribute], 'UNIQUE');
173
        // }
174
175
        // Set bind types
176
        $bindTypes = $metaData->getBindTypes($model);
177
        foreach ($bindTypes as $attribute => $value) {
178
            $table_structure[$attribute]['bindType'] = $value;
179
        }
180
181
        // Find auto incremental column, usually it is ID column
182
        $keyFiled = $metaData->getIdentityField($model);
183
        if ($keyFiled) {
184
            unset($indexes[$keyFiled]);
185
            $table_structure[$keyFiled] = [
186
                'type'          => Column::TYPE_INTEGER,
187
                'notNull'       => true,
188
                'autoIncrement' => true,
189
                'primary'       => true,
190
                'isNumeric'     => true,
191
                'first'         => true,
192
            ];
193
        }
194
195
        // Some exceptions
196
        if ($modelClassName === PbxSettings::class) {
197
            $keyFiled = 'key';
198
            unset($indexes[$keyFiled]);
199
            $table_structure[$keyFiled] = [
200
                'type'          => Column::TYPE_VARCHAR,
201
                'notNull'       => true,
202
                'autoIncrement' => false,
203
                'primary'       => true,
204
                'isNumeric'     => false,
205
                'first'         => true,
206
            ];
207
        }
208
209
        // Create additional indexes
210
        $modelClassAnnotation = $modelAnnotation->getClassAnnotations();
211
        if ($modelClassAnnotation !== false
212
            && $modelClassAnnotation->has('Indexes')) {
213
            $additionalIndexes = $modelClassAnnotation->get('Indexes')->getArguments();
214
            foreach ($additionalIndexes as $index) {
215
                $indexName           = "i_{$tableName}_{$index['name']}";
216
                $indexes[$indexName] = new Index($indexName, $index['columns'], $index['type']);
217
            }
218
        }
219
220
        // Create new table structure
221
        $columns = [];
222
        foreach ($table_structure as $colName => $colType) {
223
            $columns[] = new Column($colName, $colType);
224
        }
225
226
        $columnsNew = [
227
            'columns' => $columns,
228
            'indexes' => $indexes,
229
        ];
230
231
        $connectionService->begin();
232
233
        if ( ! $connectionService->tableExists($tableName)) {
234
            Util::echoWithSyslog(' - UpdateDatabase: Create new table: ' . $tableName . ' ');
235
            $result = $connectionService->createTable($tableName, '', $columnsNew);
236
            Util::echoGreenDone();
237
        } else {
238
            // Table exists, we have to check/upgrade its structure
239
            $currentColumnsArr = $connectionService->describeColumns($tableName, '');
240
241
            if ($this->isTableStructureNotEqual($currentColumnsArr, $columns)) {
242
                Util::echoWithSyslog(' - UpdateDatabase: Upgrade table: ' . $tableName . ' ');
243
                // Create new table and copy all data
244
                $currentStateColumnList = [];
245
                $oldColNames            = []; // Старые названия колонок
246
                $countColumnsTemp       = count($currentColumnsArr);
247
                for ($k = 0; $k < $countColumnsTemp; $k++) {
248
                    $currentStateColumnList[$k] = $currentColumnsArr[$k]->getName();
249
                    $oldColNames[]              = $currentColumnsArr[$k]->getName();
250
                }
251
252
                // Create temporary clone on current table with all columns and date
253
                // Delete original table
254
                $gluedColumns = implode(',', $currentStateColumnList);
255
                $query        = "CREATE TEMPORARY TABLE {$tableName}_backup({$gluedColumns}); 
256
INSERT INTO {$tableName}_backup SELECT {$gluedColumns} FROM {$tableName}; 
257
DROP TABLE  {$tableName}";
258
                $result       = $result && $connectionService->execute($query);
259
260
                // Create new table with new columns structure
261
                $result = $result && $connectionService->createTable($tableName, '', $columnsNew);
262
263
                // Copy data from temporary table to newly created
264
                $newColumnNames  = array_intersect($newColNames, $oldColNames);
265
                $gluedNewColumns = implode(',', $newColumnNames);
266
                $result          = $result && $connectionService->execute(
267
                        "INSERT INTO {$tableName} ( {$gluedNewColumns}) SELECT {$gluedNewColumns}  FROM {$tableName}_backup;"
268
                    );
269
270
                // Drop temporary table
271
                $result = $result && $connectionService->execute("DROP TABLE {$tableName}_backup;");
272
                Util::echoGreenDone();
273
            }
274
        }
275
276
277
        if ($result) {
278
            $this->updateIndexes($tableName, $connectionService, $indexes);
279
        }
280
281
        if ($result) {
282
            $connectionService->commit();
283
        } else {
284
            Util::sysLogMsg('createUpdateDbTableByAnnotations', "Error: Failed on create/update table {$tableName}");
285
            $connectionService->rollback();
286
        }
287
288
        return $result;
289
    }
290
291
    /**
292
     * Compare database structure with metadata info
293
     *
294
     * @param $currentTableStructure
295
     * @param $newTableStructure
296
     *
297
     * @return bool
298
     */
299
    private function isTableStructureNotEqual($currentTableStructure, $newTableStructure): bool
300
    {
301
        //1. Check fields count
302
        if (count($currentTableStructure) !== count($newTableStructure)) {
303
            return true;
304
        }
305
306
        $comparedSettings = [
307
            'getName',
308
            'getType',
309
            'getTypeReference',
310
            'getTypeValues',
311
            'getSize',
312
            'getScale',
313
            'isUnsigned',
314
            'isNotNull',
315
            'isPrimary',
316
            'isAutoIncrement',
317
            'isNumeric',
318
            'isFirst',
319
            'getAfterPosition',
320
            //'getBindType',
321
            'getDefault',
322
            'hasDefault',
323
        ];
324
325
        //2. Check fields types
326
        foreach ($newTableStructure as $index => $newField) {
327
            $oldField = $currentTableStructure[$index];
328
            foreach ($comparedSettings as $compared_setting) {
329
                if ($oldField->$compared_setting() !== $newField->$compared_setting()) {
330
                    // Sqlite transform "1" to ""1"" in default settings, but it is normal
331
                    if ($compared_setting === 'getDefault'
332
                        && $oldField->$compared_setting() === '"' . $newField->$compared_setting() . '"') {
333
                        continue;
334
                    }
335
336
                    // Description for "length" is integer, but table structure store it as string
337
                    if ($compared_setting === 'getSize'
338
                        && (string)$oldField->$compared_setting() === (string)$newField->$compared_setting()) {
339
                        continue;
340
                    }
341
342
                    return true; // find different columns
343
                }
344
            }
345
        }
346
347
        return false;
348
    }
349
350
351
    /**
352
     * @param string $tableName
353
     * @param mixed  $connectionService DependencyInjection connection service used to read data
354
     * @param array  $indexes
355
     *
356
     * @return bool
357
     */
358
    private function updateIndexes(string $tableName, $connectionService, array $indexes): bool
359
    {
360
        $result         = true;
361
        $currentIndexes = $connectionService->describeIndexes($tableName);
362
363
        // Drop not exist indexes
364
        foreach ($currentIndexes as $indexName => $currentIndex) {
365
            if (stripos($indexName, 'sqlite_autoindex') === false
366
                && ! array_key_exists($indexName, $indexes)
367
            ) {
368
                Util::echoWithSyslog(" - UpdateDatabase: Delete index: {$indexName} ");
369
                $result = $result + $connectionService->dropIndex($tableName, '', $indexName);
370
                Util::echoGreenDone();
371
            }
372
        }
373
374
        // Add/update exist indexes
375
        foreach ($indexes as $indexName => $describedIndex) {
376
            if (array_key_exists($indexName, $currentIndexes)) {
377
                $currentIndex = $currentIndexes[$indexName];
378
                if ($describedIndex->getColumns() !== $currentIndex->getColumns()) {
379
                    Util::echoWithSyslog(" - UpdateDatabase: Update index: {$indexName} ");
380
                    $result = $result + $connectionService->dropIndex($tableName, '', $indexName);
381
                    $result = $result + $connectionService->addIndex($tableName, '', $describedIndex);
382
                    Util::echoGreenDone();
383
                }
384
            } else {
385
                Util::echoWithSyslog(" - UpdateDatabase: Add new index: {$indexName} ");
386
                $result = $result + $connectionService->addIndex($tableName, '', $describedIndex);
387
                Util::echoGreenDone();
388
            }
389
        }
390
391
        return $result;
392
    }
393
}