Starting from Laravel 11, migrations are required to include all attributes of a column in such cases, even those that are not being modified. Otherwise, those attributes will be lost. However, this poses a challenge for me, as I don't want to constantly look up all the attributes of the column and define them explicitly.
For example, imagine you have a migration that creates a votes column with the unsigned, default, and comment attributes:
Schema::create('users', function (Blueprint $table) { $table->integer('votes')->unsigned()->default(1)->comment('The vote count'); });
Later, you write a migration that changes the column to be nullable as well:
Schema::table('users', function (Blueprint $table) { $table->integer('votes')->nullable()->change(); });
In Laravel 10, this migration would retain the unsigned, default, and comment attributes on the column. However, in Laravel 11, the migration must now also include all of the attributes that were previously defined on the column. Otherwise, they will be dropped:
Schema::table('users', function (Blueprint $table) { $table->integer('votes') ->unsigned() ->default(1) ->comment('The vote count') ->nullable() ->change(); });
How can I preserve the original column-settings when updating the attributes of an existing column in a migration from Laravel 11?
Schema::table('users', function (Blueprint $table) {
$table->integer('votes')->nullable()->change();
});
The expected result is that, alongside setting the column to nullable
, it automatically retains the unsigned
, default(1)
, and comment
values as well. And I don't even want to look up the column type for such a minor change-whether it's a string, integer, etc., its exact length, and so on.
This will be a somewhat lengthy answer, as multiple steps are required to implement this. In short, the idea is to create a macro for the Blueprint
class named column
.
Using this macro, we can instantly fetch all the current properties from the database and associate them with the column so that you don't have to manually specify them again.
column
macro for BlueprintBlueprint::macro('column', function (string $column) {
/** @var \Illuminate\Database\Schema\Blueprint $this */
$table = $this->getTable();
// Based on the column name and $this->getTable(), all data about the column can be retrieved from the database, including its type, indexes, etc.
$definition = $this->addColumn($type, $column, $parameters);
// $definition->nullable(true/false);
// $definition->autoIncrement(true/false);
// etc. here
return $definition->change();
});
To create the definition
, we need to know the column type and other parameters that will be used to define the column in Laravel, e.g., string
; length: 100
.
The column
macro should return a valid definition, which we can then customize. I suggest returning it with a ->change()
call right away, as the column
macro is only used for modifying columns, so there's no point in postponing it.
Between the creation of the $definition
and the return
, we need to set the settings queried from the SQL for the $definition
, such as unique
, primary key
, unsigned
, etc. Otherwise, due to changes made in Laravel 11, these settings would be "deleted", meaning they wouldn't be applied, even if they were already set for the column. This explanation is the longest, so I have written it separately:
Querying column data using $this
and Schema
/** @var \Illuminate\Database\Schema\Blueprint $this */
$table = $this->getTable();
// The schema returns the data for all columns of the table (unfortunately)
$columns = Schema::getColumns($table); // This actually gives quite a limited result, as it doesn't reveal whether the column is unique or a primary key from a single boolean variable, and this is what makes the response more complex.
// Finding the column with the current name
$columnData = collect($columns)->firstWhere('name', $column);
// And it's necessary to have the column's "Laravel" type, for example, for a varchar column, this would mean string in the Blueprint... I have only shared the logic
// $columnData['type_name'] example: varchar what will string
$type_laravel_name = match ($columnData['type_name']) {
'varchar', 'char' => 'string',
'text', 'smalltext', 'mediumtext', 'longtext' => 'text',
'tinyint' => 'tinyInteger',
...
};
// And based on the type, the details also need to be determined (for example, the length for a varchar), I have only shared the logic
// $columnData['type'] example: varchar(100) where details: ['length' => 100]
$type_details = match ($type_laravel_name) => {
'string' => [
'length' => (int) Str::of($columnData['type'])->match('/\((\d+)\)/')->toString() ?: null,
'compressed' => Str::of($columnData['type'])->lower()->contains('compressed'),
],
'float' => [
'precision' => (int) Str::of($columnData['type'])->match('/\((\d+)\)/')->toString(),
],
// ...
};
// The $type_laravel_name and $type_details are necessary for creating the $definition, so we don't have to manually specify again that we're dealing with a varchar that is 100 characters long
$definition = $this->addColumn($type_laravel_name, $column, $type_details);
Setting the current configs that can be queried
// Is nullable?
$definition->nullable($columnData['nullable']);
// Has default value?
if (! is_null($columnData['default'])) {
// Handle special cases becuase $columnData['default'] will be a string in every case
$defaultValue = match ($type_laravel_name) {
'int', 'smallInteger', '...' => (int) $default,
'float', 'double', 'decimal' => (float) $default,
'boolean', 'bool' => filter_var($default, FILTER_VALIDATE_BOOLEAN),
'json' => json_decode($default, true) ?? '{}',
'binary', 'varbinary', 'blob' => base64_decode($default),
default => $default, // Leave unknown types unchanged
};
$definition->default($default);
}
$definition->autoIncrement($columnData['auto_increment']);
// Is unsigned?
$definition->unsigned(Str::of($columnData['type'])->lower()->contains('unsigned'));
// Has comment?
if (! is_null($columnData['comment'])) {
$definition->comment($columnData['comment']);
}
// And once everything is set, we can return. These settings can be overridden again outside of the migration, if necessary
return $definition->change();
In a migration, when modifying a column using the column
macro, there is no need to call the change
function or set other parameters. We have the ability to modify the set parameters, such as the type
, or for example, the length
for a varchar, whether it is nullable
, and so on.
Schema::table('table', function (Blueprint $table) {
$table->column('column')->type('string')->length(100)->autoIncrement(false);
$table->column('another_1_column')->nullable();
$table->column('another_2_column')->nullable(false);
// ...
});
ColumnDefinition
to prevent the IDE from showing warnings when using otherwise existing methods.In the first instance, I needed to externally extend the PHPDoc content of the ColumnDefinition
. The ColumnDefinition
is built on Fluent, allowing variables to be managed within it. Values like unsigned
or autoIncrement
that can be assigned to a column also fall into this category. By default, Laravel conceals the fact that these variables can accept boolean values, which is understandable since the new Schema mechanism requires specifying all necessary properties for every migration; otherwise, they are lost.
However, my modification assigns the current properties to the column when it is being modified. This necessitates that all parameters, including true/false and other customizability options, are available beyond their default values for property removal. I raised a separate question regarding the PHPDoc extension needed for this purpose, which can be read at Expanding the PHPDoc of a PHP class what found in a Composer package.
For this solution, the column
macro definition needs to be added to the Blueprint
, and the extended autoIncrement
and unsigned
specifications need to be added to the ColumnDefinition
, which look like this:
// ide-helper.php
/**
* @method \Illuminate\Database\Schema\ColumnDefinition column(string $column) Set column properties for change
*/
class Blueprint
{
//
}
if (! class_exists(Blueprint::class)) {
class_alias(Blueprint::class, \Illuminate\Database\Schema\Blueprint::class);
}
/**
* @method $this autoIncrement(bool $value = true) Set INTEGER columns as auto-increment (primary key) or NON-auto-increment
* @method $this unsigned(bool $value = true) Set the INTEGER column as UNSIGNED (MySQL) or NON-UNSIGNED
*/
class ColumnDefinition
{
//
}
if (! class_exists(ColumnDefinition::class)) {
class_alias(ColumnDefinition::class, \Illuminate\Database\Schema\ColumnDefinition::class);
}
In Laravel 10, this migration would retain the unsigned, default, and comment attributes on the column. However, in Laravel 11, the migration must now also include all of the attributes that were previously defined on the column. Otherwise, they will be dropped:
Schema::table('users', function (Blueprint $table) { $table->integer('votes') ->unsigned() ->default(1) ->comment('The vote count') ->nullable() ->change(); });
However, with my column
macro, if I only want to modify the nullable
on an existing column and keep all other attributes intact, the modification would look like this:
Schema::table('users', function (Blueprint $table) {
$table->column('votes')->nullable();
});