Passed
Pull Request — master (#16)
by Nikolay
13:10 queued 02:12
created

UpdateDatabase::createUpdateDbTableByAnnotations()   F

Complexity

Conditions 32
Paths > 20000

Size

Total Lines 208
Code Lines 124

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 124
c 2
b 0
f 0
dl 0
loc 208
rs 0
cc 32
nc 110597
nop 1

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