1<?php
2
3namespace Illuminate\Database\Schema\Grammars;
4
5use Doctrine\DBAL\Schema\AbstractSchemaManager as SchemaManager;
6use Doctrine\DBAL\Schema\Comparator;
7use Doctrine\DBAL\Schema\Table;
8use Doctrine\DBAL\Types\Type;
9use Illuminate\Database\Connection;
10use Illuminate\Database\Schema\Blueprint;
11use Illuminate\Support\Fluent;
12use RuntimeException;
13
14class ChangeColumn
15{
16    /**
17     * Compile a change column command into a series of SQL statements.
18     *
19     * @param  \Illuminate\Database\Schema\Grammars\Grammar  $grammar
20     * @param  \Illuminate\Database\Schema\Blueprint  $blueprint
21     * @param  \Illuminate\Support\Fluent  $command
22     * @param  \Illuminate\Database\Connection  $connection
23     * @return array
24     *
25     * @throws \RuntimeException
26     */
27    public static function compile($grammar, Blueprint $blueprint, Fluent $command, Connection $connection)
28    {
29        if (! $connection->isDoctrineAvailable()) {
30            throw new RuntimeException(sprintf(
31                'Changing columns for table "%s" requires Doctrine DBAL. Please install the doctrine/dbal package.',
32                $blueprint->getTable()
33            ));
34        }
35
36        $schema = $connection->getDoctrineSchemaManager();
37        $databasePlatform = $schema->getDatabasePlatform();
38        $databasePlatform->registerDoctrineTypeMapping('enum', 'string');
39
40        $tableDiff = static::getChangedDiff(
41            $grammar, $blueprint, $schema
42        );
43
44        if ($tableDiff !== false) {
45            return (array) $databasePlatform->getAlterTableSQL($tableDiff);
46        }
47
48        return [];
49    }
50
51    /**
52     * Get the Doctrine table difference for the given changes.
53     *
54     * @param  \Illuminate\Database\Schema\Grammars\Grammar  $grammar
55     * @param  \Illuminate\Database\Schema\Blueprint  $blueprint
56     * @param  \Doctrine\DBAL\Schema\AbstractSchemaManager  $schema
57     * @return \Doctrine\DBAL\Schema\TableDiff|bool
58     */
59    protected static function getChangedDiff($grammar, Blueprint $blueprint, SchemaManager $schema)
60    {
61        $current = $schema->listTableDetails($grammar->getTablePrefix().$blueprint->getTable());
62
63        return (new Comparator)->diffTable(
64            $current, static::getTableWithColumnChanges($blueprint, $current)
65        );
66    }
67
68    /**
69     * Get a copy of the given Doctrine table after making the column changes.
70     *
71     * @param  \Illuminate\Database\Schema\Blueprint  $blueprint
72     * @param  \Doctrine\DBAL\Schema\Table  $table
73     * @return \Doctrine\DBAL\Schema\Table
74     */
75    protected static function getTableWithColumnChanges(Blueprint $blueprint, Table $table)
76    {
77        $table = clone $table;
78
79        foreach ($blueprint->getChangedColumns() as $fluent) {
80            $column = static::getDoctrineColumn($table, $fluent);
81
82            // Here we will spin through each fluent column definition and map it to the proper
83            // Doctrine column definitions - which is necessary because Laravel and Doctrine
84            // use some different terminology for various column attributes on the tables.
85            foreach ($fluent->getAttributes() as $key => $value) {
86                if (! is_null($option = static::mapFluentOptionToDoctrine($key))) {
87                    if (method_exists($column, $method = 'set'.ucfirst($option))) {
88                        $column->{$method}(static::mapFluentValueToDoctrine($option, $value));
89                        continue;
90                    }
91
92                    $column->setCustomSchemaOption($option, static::mapFluentValueToDoctrine($option, $value));
93                }
94            }
95        }
96
97        return $table;
98    }
99
100    /**
101     * Get the Doctrine column instance for a column change.
102     *
103     * @param  \Doctrine\DBAL\Schema\Table  $table
104     * @param  \Illuminate\Support\Fluent  $fluent
105     * @return \Doctrine\DBAL\Schema\Column
106     */
107    protected static function getDoctrineColumn(Table $table, Fluent $fluent)
108    {
109        return $table->changeColumn(
110            $fluent['name'], static::getDoctrineColumnChangeOptions($fluent)
111        )->getColumn($fluent['name']);
112    }
113
114    /**
115     * Get the Doctrine column change options.
116     *
117     * @param  \Illuminate\Support\Fluent  $fluent
118     * @return array
119     */
120    protected static function getDoctrineColumnChangeOptions(Fluent $fluent)
121    {
122        $options = ['type' => static::getDoctrineColumnType($fluent['type'])];
123
124        if (in_array($fluent['type'], ['text', 'mediumText', 'longText'])) {
125            $options['length'] = static::calculateDoctrineTextLength($fluent['type']);
126        }
127
128        if (static::doesntNeedCharacterOptions($fluent['type'])) {
129            $options['customSchemaOptions'] = [
130                'collation' => '',
131                'charset' => '',
132            ];
133        }
134
135        return $options;
136    }
137
138    /**
139     * Get the doctrine column type.
140     *
141     * @param  string  $type
142     * @return \Doctrine\DBAL\Types\Type
143     */
144    protected static function getDoctrineColumnType($type)
145    {
146        $type = strtolower($type);
147
148        switch ($type) {
149            case 'biginteger':
150                $type = 'bigint';
151                break;
152            case 'smallinteger':
153                $type = 'smallint';
154                break;
155            case 'mediumtext':
156            case 'longtext':
157                $type = 'text';
158                break;
159            case 'binary':
160                $type = 'blob';
161                break;
162            case 'uuid':
163                $type = 'guid';
164                break;
165        }
166
167        return Type::getType($type);
168    }
169
170    /**
171     * Calculate the proper column length to force the Doctrine text type.
172     *
173     * @param  string  $type
174     * @return int
175     */
176    protected static function calculateDoctrineTextLength($type)
177    {
178        switch ($type) {
179            case 'mediumText':
180                return 65535 + 1;
181            case 'longText':
182                return 16777215 + 1;
183            default:
184                return 255 + 1;
185        }
186    }
187
188    /**
189     * Determine if the given type does not need character / collation options.
190     *
191     * @param  string  $type
192     * @return bool
193     */
194    protected static function doesntNeedCharacterOptions($type)
195    {
196        return in_array($type, [
197            'bigInteger',
198            'binary',
199            'boolean',
200            'date',
201            'decimal',
202            'double',
203            'float',
204            'integer',
205            'json',
206            'mediumInteger',
207            'smallInteger',
208            'time',
209            'tinyInteger',
210        ]);
211    }
212
213    /**
214     * Get the matching Doctrine option for a given Fluent attribute name.
215     *
216     * @param  string  $attribute
217     * @return string|null
218     */
219    protected static function mapFluentOptionToDoctrine($attribute)
220    {
221        switch ($attribute) {
222            case 'type':
223            case 'name':
224                return;
225            case 'nullable':
226                return 'notnull';
227            case 'total':
228                return 'precision';
229            case 'places':
230                return 'scale';
231            default:
232                return $attribute;
233        }
234    }
235
236    /**
237     * Get the matching Doctrine value for a given Fluent attribute.
238     *
239     * @param  string  $option
240     * @param  mixed  $value
241     * @return mixed
242     */
243    protected static function mapFluentValueToDoctrine($option, $value)
244    {
245        return $option === 'notnull' ? ! $value : $value;
246    }
247}
248