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