Completed
Push — master ( 91cc9b...471c88 )
by Henry
02:29
created

SQL::getFullTextColumns()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 4
nop 1
dl 0
loc 16
rs 9.2
c 0
b 0
f 0
1
<?php
2
/*
3
 * This file is part of the Divergence package.
4
 *
5
 * (c) Henry Paradiz <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Divergence\IO\Database;
12
13
use Exception;
14
15
/**
16
 * SQL.
17
 * @package Divergence
18
 * @author  Henry Paradiz <[email protected]>
19
 * @author  Chris Alfano <[email protected]>
20
 *
21
 */
22
23
class SQL
24
{
25
    protected static $aggregateFieldConfigs;
26
27
    /**
28
     * This is how MySQL escapes it's string under the hood.
29
     * Keep it. We don't need a database connection to escape strings.
30
     *
31
     * @param string $str String to escape.
32
     * @return string Escaped string.
33
     */
34
    public static function escape($str)
35
    {
36
        return str_replace(
37
            ["\\",  "\x00", "\n",  "\r",  "'",  '"', "\x1a"],
38
            ["\\\\","\\0","\\n", "\\r", "\'", '\"', "\\Z"],
39
            $str
40
        );
41
    }
42
43
    public static function compileFields($recordClass,$historyVariant = false)
44
    {
45
        $queryString = [];
46
        $fields = static::getAggregateFieldOptions($recordClass);
47
48
        foreach ($fields as $fieldId => $field) {
49
            if ($field['columnName'] == 'RevisionID') {
50
                continue;
51
            }
52
53
            $queryString[] = static::getFieldDefinition($recordClass, $fieldId, $historyVariant);
54
55
            if (!empty($field['primary'])) {
56
                if ($historyVariant) {
57
                    $queryString[] = 'KEY `'.$field['columnName'].'` (`'.$field['columnName'].'`)';
58
                } else {
59
                    $queryString[] = 'PRIMARY KEY (`'.$field['columnName'].'`)';
60
                }
61
            }
62
63
            if (!empty($field['unique']) && !$historyVariant) {
64
                $queryString[] = 'UNIQUE KEY `'.$field['columnName'].'` (`'.$field['columnName'].'`)';
65
            }
66
67
            if (!empty($field['index']) && !$historyVariant) {
68
                $queryString[] = 'KEY `'.$field['columnName'].'` (`'.$field['columnName'].'`)';
69
            }
70
        }
71
72
        return $queryString;
73
    }
74
75
    public static function getFullTextColumns($recordClass)
76
    {
77
        $fulltextColumns = [];
78
        $fields = static::getAggregateFieldOptions($recordClass);
79
80
        foreach ($fields as $fieldId => $field) {
81
            if ($field['columnName'] == 'RevisionID') {
82
                continue;
83
            }
84
85
            if (!empty($field['fulltext'])) {
86
                $fulltextColumns[] = $field['columnName'];
87
            }
88
        }
89
90
        return $fulltextColumns;
91
    }
92
93
    public static function getContextIndex($recordClass)
94
    {
95
        return 'KEY `CONTEXT` (`'.$recordClass::getColumnName('ContextClass').'`,`'.$recordClass::getColumnName('ContextID').'`)';
96
    }
97
98
    /**
99
     * Generates a MySQL create table query from a Divergence\Models\ActiveRecord class.
100
     *
101
     * @param string $recordClass Class name
102
     * @param boolean $historyVariant
103
     * @return void
104
     */
105
    public static function getCreateTable($recordClass, $historyVariant = false)
106
    {
107
        $indexes = $historyVariant ? [] : $recordClass::$indexes;
0 ignored issues
show
Bug introduced by
The property indexes does not exist on string.
Loading history...
108
        $fulltextColumns = [];
109
        $queryString = [];
110
111
112
        // history table revisionID field
113
        if ($historyVariant) {
114
            $queryString[] = '`RevisionID` int(10) unsigned NOT NULL auto_increment';
115
            $queryString[] = 'PRIMARY KEY (`RevisionID`)';
116
        }
117
118
        $queryString = array_merge($queryString,static::compileFields($recordClass, $historyVariant));
119
120
        if(!$historyVariant) {
121
            // If ContextClass && ContextID are members of this model let's index them
122
            if ($recordClass::fieldExists('ContextClass') && $recordClass::fieldExists('ContextID')) {
123
                $queryString[] = static::getContextIndex($recordClass);
124
            }
125
126
            $fulltextColumns = static::getFullTextColumns($recordClass);
127
        }
128
129
        // compile indexes
130
        foreach ($indexes as $indexName => $index) {
131
132
            // translate field names
133
            foreach ($index['fields'] as &$indexField) {
134
                $indexField = $recordClass::getColumnName($indexField);
135
            }
136
137
            if (!empty($index['fulltext'])) {
138
                $fulltextColumns = array_unique(array_merge($fulltextColumns, $index['fields']));
139
                continue;
140
            }
141
142
            $queryString[] = sprintf(
143
                '%s KEY `%s` (`%s`)',
144
                !empty($index['unique']) ? 'UNIQUE' : '',
145
                $indexName,
146
                join('`,`', $index['fields'])
147
            );
148
        }
149
150
        if (!empty($fulltextColumns)) {
151
            $queryString[] = 'FULLTEXT KEY `FULLTEXT` (`'.join('`,`', $fulltextColumns).'`)';
152
        }
153
154
155
        $createSQL = sprintf(
156
            "CREATE TABLE IF NOT EXISTS `%s` (\n\t%s\n) ENGINE=MyISAM DEFAULT CHARSET=utf8;",
157
            $historyVariant ? $recordClass::getHistoryTable() : $recordClass::$tableName,
0 ignored issues
show
Bug introduced by
The property tableName does not exist on string.
Loading history...
158
            join("\n\t,", $queryString)
159
        );
160
161
        // append history table SQL
162
        if (!$historyVariant && is_subclass_of($recordClass, 'VersionedRecord')) {
163
            $createSQL .= PHP_EOL.PHP_EOL.PHP_EOL.static::getCreateTable($recordClass, true);
0 ignored issues
show
Bug introduced by
Are you sure the usage of static::getCreateTable($recordClass, true) targeting Divergence\IO\Database\SQL::getCreateTable() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
Are you sure static::getCreateTable($recordClass, true) of type void can be used in concatenation? ( Ignorable by Annotation )

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

163
            $createSQL .= PHP_EOL.PHP_EOL.PHP_EOL./** @scrutinizer ignore-type */ static::getCreateTable($recordClass, true);
Loading history...
164
        }
165
        return $createSQL;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $createSQL returns the type string which is incompatible with the documented return type void.
Loading history...
166
    }
167
168
    public static function getSQLType($field)
169
    {
170
        switch ($field['type']) {
171
            case 'boolean':
172
                return 'boolean';
173
            case 'tinyint':
174
            case 'smallint':
175
            case 'mediumint':
176
            case 'bigint':
177
                return $field['type'].($field['unsigned'] ? ' unsigned' : '').($field['zerofill'] ? ' zerofill' : '');
178
            case 'uint':
179
                $field['unsigned'] = true;
180
                // no break
181
            case 'int':
182
            case 'integer':
183
                return 'int'.($field['unsigned'] ? ' unsigned' : '').(!empty($field['zerofill']) ? ' zerofill' : '');
184
            case 'decimal':
185
                return sprintf('decimal(%s,%s)', $field['precision'], $field['scale']);
186
            case 'float':
187
                return 'float';
188
            case 'double':
189
                return 'double';
190
191
            case 'password':
192
            case 'string':
193
            case 'varchar':
194
            case 'list':
195
                return sprintf(!$field['length'] || $field['type'] == 'varchar' ? 'varchar(%u)' : 'char(%u)', $field['length'] ? $field['length'] : 255);
196
            case 'clob':
197
            case 'serialized':
198
            case 'json':
199
                return 'text';
200
            case 'blob':
201
                return 'blob';
202
203
            case 'timestamp':
204
                return 'timestamp';
205
            case 'datetime':
206
                return 'datetime';
207
            case 'time':
208
                return 'time';
209
            case 'date':
210
                return 'date';
211
            case 'year':
212
                return 'year';
213
214
            case 'enum':
215
                return sprintf('enum("%s")', join('","', array_map([static::class,'escape'], $field['values'])));
216
217
            case 'set':
218
                return sprintf('set("%s")', join('","', array_map([static::class,'escape'], $field['values'])));
219
220
            default:
221
                throw new Exception("getSQLType: unhandled type $field[type]");
222
        }
223
    }
224
225
    public static function getFieldDefinition($recordClass, $fieldName, $historyVariant = false)
226
    {
227
        $field = static::getAggregateFieldOptions($recordClass, $fieldName);
228
        $rootClass = $recordClass::$rootClass;
229
230
        // force notnull=false on non-rootclass fields
231
        if ($rootClass && !$rootClass::fieldExists($fieldName)) {
232
            $field['notnull'] = false;
233
        }
234
235
        // auto-prepend class type
236
        if ($field['columnName'] == 'Class' && $field['type'] == 'enum' && !in_array($rootClass, $field['values']) && !count($rootClass::getStaticSubClasses())) {
237
            array_unshift($field['values'], $rootClass);
238
        }
239
240
        $fieldDef = '`'.$field['columnName'].'`';
241
        $fieldDef .= ' '.static::getSQLType($field);
242
243
        if (!empty($field['charset'])) {
244
            $fieldDef .= " CHARACTER SET $field[charset]";
245
        }
246
247
        if (!empty($field['collate'])) {
248
            $fieldDef .= " COLLATE $field[collate]";
249
        }
250
251
        $fieldDef .= ' '.($field['notnull'] ? 'NOT NULL' : 'NULL');
252
253
        if ($field['autoincrement'] && !$historyVariant) {
254
            $fieldDef .= ' auto_increment';
255
        } elseif (($field['type'] == 'timestamp') && ($field['default'] == 'CURRENT_TIMESTAMP')) {
256
            $fieldDef .= ' default CURRENT_TIMESTAMP';
257
        } elseif (empty($field['notnull']) && ($field['default'] == null)) {
258
            $fieldDef .= ' default NULL';
259
        } elseif (isset($field['default'])) {
260
            if ($field['type'] == 'boolean') {
261
                $fieldDef .= ' default '.($field['default'] ? 1 : 0);
262
            } else {
263
                $fieldDef .= ' default "'.static::escape($field['default']).'"';
264
            }
265
        }
266
267
        return $fieldDef;
268
    }
269
270
    protected static function getAggregateFieldOptions($recordClass, $field = null)
271
    {
272
        if (!isset(static::$aggregateFieldConfigs[$recordClass])) {
273
            static::$aggregateFieldConfigs[$recordClass] = $recordClass::getClassFields();
274
        }
275
276
        if ($field) {
277
            return static::$aggregateFieldConfigs[$recordClass][$field];
278
        } else {
279
            return static::$aggregateFieldConfigs[$recordClass];
280
        }
281
    }
282
}
283