Completed
Push — master ( 4df908...713b25 )
by Anton
01:54
created

TableRenderer::renderColumn()   B

Complexity

Conditions 10
Paths 12

Size

Total Lines 41
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 19
nc 12
nop 4
dl 0
loc 41
rs 7.6666
c 0
b 0
f 0

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
declare(strict_types=1);
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
namespace Cycle\ORM\Tests\Util;
11
12
use Cycle\ORM\Exception\SchemaException;
13
use Spiral\Database\Schema\AbstractColumn;
14
use Spiral\Database\Schema\AbstractTable;
15
16
/**
17
 * Implements the ability to define column in AbstractSchema based on string representation and
18
 * default value (if defined).
19
 *
20
 * Attention, this class will try to guess default value if column is NOT NULL and no default
21
 * value provided by user.
22
 */
23
final class TableRenderer
24
{
25
    /**
26
     * Render columns in table based on string definition.
27
     *
28
     * Example:
29
     * renderColumns(
30
     *      $table,
31
     *      [
32
     *          'id'     => 'primary',
33
     *          'time'   => 'datetime, nullable',
34
     *          'status' => 'enum(active, disabled)'
35
     *      ],
36
     *      [
37
     *          'status' => 'active',
38
     *          'time'   => null,
39
     *      ]
40
     * );
41
     *
42
     * @param AbstractTable $table
43
     * @param array         $columns
44
     * @param array         $defaults
45
     *
46
     * @throws SchemaException
47
     */
48
    public function renderColumns(AbstractTable $table, array $columns, array $defaults)
49
    {
50
        $primaryKeys = [];
51
        foreach ($columns as $name => $definition) {
52
            $type = $this->parse($table->getName(), $name, $definition);
53
54
            if ($this->hasFlag($type, 'primary')) {
55
                $primaryKeys[] = $name;
56
            }
57
58
            $this->renderColumn(
59
                $table->column($name),
60
                $type,
61
                array_key_exists($name, $defaults),
62
                $defaults[$name] ?? null
63
            );
64
        }
65
66
        if (count($primaryKeys)) {
67
            $table->setPrimaryKeys($primaryKeys);
68
        }
69
    }
70
71
    /**
72
     * Cast (specify) column schema based on provided column definition and default value.
73
     * Spiral will force default values (internally) for every NOT NULL column except primary keys!
74
     *
75
     * Column definition are compatible with database Migrations and AbstractColumn types.
76
     *
77
     * Column definition examples (by default all columns has flag NOT NULL):
78
     * const SCHEMA = [
79
     *      'id'           => 'primary',
80
     *      'name'         => 'string',                          //Default length is 255 characters.
81
     *      'email'        => 'string(255), nullable',           //Can be NULL
82
     *      'status'       => 'enum(active, pending, disabled)', //Enum values, trimmed
83
     *      'balance'      => 'decimal(10, 2)',
84
     *      'message'      => 'text, null',                      //Alias for nullable
85
     *      'time_expired' => 'timestamp'
86
     * ];
87
     *
88
     * Attention, column state will be affected!
89
     *
90
     * @see  AbstractColumn
91
     * @param AbstractColumn $column
92
     * @param array          $type
93
     * @param bool           $hasDefault Must be set to true if default value was set by user.
94
     * @param mixed          $default    Default value declared by record schema.
95
     *
96
     * @throws SchemaException
97
     */
98
    protected function renderColumn(AbstractColumn $column, array $type, bool $hasDefault, $default = null)
99
    {
100
        // ORM force EVERY column to NOT NULL state unless different is said
101
        $column->nullable(false);
102
103
        if ($this->hasFlag($type, 'null') || $this->hasFlag($type, 'nullable')) {
104
            // indication that column is nullable
105
            $column->nullable(true);
106
        }
107
108
        try {
109
            // bypassing call to AbstractColumn->__call method (or specialized column method)
110
            call_user_func_array([$column, $type['type']], $type['options']);
111
        } catch (\Throwable $e) {
112
            throw new SchemaException(
113
                "Invalid column type definition in '{$column->getTable()}'.'{$column->getName()}'",
114
                $e->getCode(),
115
                $e
116
            );
117
        }
118
119
        if (in_array($column->getAbstractType(), ['primary', 'bigPrimary'])) {
120
            // no default value can be set of primary keys
121
            return;
122
        }
123
124
        if (!$hasDefault && !$column->isNullable()) {
125
            if (!$this->hasFlag($type, 'required') && !$this->hasFlag($type, 'primary')) {
126
                // we have to come up with some default value
127
                $column->defaultValue($this->castDefault($column));
128
            }
129
130
            return;
131
        }
132
133
        if (is_null($default)) {
134
            // default value is stated and NULL, clear what to do
135
            $column->nullable(true);
136
        }
137
138
        $column->defaultValue($default);
139
    }
140
141
    /**
142
     * @param string $table
143
     * @param string $column
144
     * @param string $definition
145
     * @return array
146
     */
147
    protected function parse(string $table, string $column, string $definition): array
148
    {
149
        if (!preg_match(
150
            '/(?P<type>[a-z]+)(?: *\((?P<options>[^\)]+)\))?(?: *, *(?P<flags>.+))?/i',
151
            $definition,
152
            $type
153
        )) {
154
            throw new SchemaException("Invalid column type definition in '{$table}'.'{$column}'");
155
        }
156
157
        if (empty($type['options'])) {
158
            $type['options'] = [];
159
        } else {
160
            $type['options'] = array_map('trim', explode(',', $type['options'] ?? ''));
161
        }
162
163
        if (empty($type['flags'])) {
164
            $type['flags'] = [];
165
        } else {
166
            $type['flags'] = array_map('trim', explode(',', $type['flags'] ?? ''));
167
        }
168
169
        unset($type[0], $type[1], $type[2], $type[3]);
170
171
        return $type;
172
    }
173
174
    /**
175
     * @param array  $type
176
     * @param string $flag
177
     * @return bool
178
     */
179
    protected function hasFlag(array $type, string $flag): bool
180
    {
181
        return in_array($flag, $type['flags'], true);
182
    }
183
184
    /**
185
     * Cast default value based on column type. Required to prevent conflicts when not nullable
186
     * column added to existed table with data in.
187
     *
188
     * @param AbstractColumn $column
189
     *
190
     * @return mixed
191
     */
192
    protected function castDefault(AbstractColumn $column)
193
    {
194
        if (in_array($column->getAbstractType(), ['timestamp', 'datetime', 'time', 'date'])) {
195
            return 0;
196
        }
197
198
        if ($column->getAbstractType() == 'enum') {
199
            // we can use first enum value as default
200
            return $column->getEnumValues()[0];
201
        }
202
203
        switch ($column->phpType()) {
204
            case 'int':
205
                return 0;
206
            case 'float':
207
                return 0.0;
208
            case 'bool':
209
                return false;
210
        }
211
212
        return '';
213
    }
214
}