diff --git a/docs/en/index.rst b/docs/en/index.rst
index 429c03ba..0eff6a92 100644
--- a/docs/en/index.rst
+++ b/docs/en/index.rst
@@ -218,7 +218,7 @@ also edit the migration after generation to add or customize the columns
Columns on the command line follow the following pattern::
- fieldName:fieldType?[length]:indexType:indexName
+ fieldName:fieldType?[length]:default[value]:indexType:indexName
For instance, the following are all valid ways of specifying an email field:
@@ -238,6 +238,16 @@ Columns with a question mark after the fieldType will make the column nullable.
The ``length`` part is optional and should always be written between bracket.
+The ``default[value]`` part is optional and sets the default value for the column.
+Supported value types include:
+
+* Booleans: ``true`` or ``false`` - e.g., ``active:boolean:default[true]``
+* Integers: ``0``, ``123``, ``-456`` - e.g., ``count:integer:default[0]``
+* Floats: ``1.5``, ``-2.75`` - e.g., ``rate:decimal:default[1.5]``
+* Strings: ``'hello'`` or ``"world"`` (quoted) - e.g., ``status:string:default['pending']``
+* Null: ``null`` or ``NULL`` - e.g., ``description:text?:default[null]``
+* SQL expressions: ``CURRENT_TIMESTAMP`` - e.g., ``created_at:datetime:default[CURRENT_TIMESTAMP]``
+
Fields named ``created`` and ``modified``, as well as any field with a ``_at``
suffix, will automatically be set to the type ``datetime``.
@@ -318,6 +328,39 @@ will generate::
}
}
+Adding a column with a default value
+-------------------------------------
+
+You can specify default values for columns using the ``default[value]`` syntax:
+
+.. code-block:: bash
+
+ bin/cake bake migration AddActiveToUsers active:boolean:default[true]
+
+will generate::
+
+ table('users');
+ $table->addColumn('active', 'boolean', [
+ 'default' => true,
+ 'null' => false,
+ ]);
+ $table->update();
+ }
+ }
+
+You can combine default values with other options like nullable and indexes:
+
+.. code-block:: bash
+
+ bin/cake bake migration AddStatusToOrders status:string:default['pending']:unique
+
Altering a column
-----------------
diff --git a/src/Command/BakeMigrationCommand.php b/src/Command/BakeMigrationCommand.php
index 024b5222..dd0e7683 100644
--- a/src/Command/BakeMigrationCommand.php
+++ b/src/Command/BakeMigrationCommand.php
@@ -174,7 +174,7 @@ public function getOptionParser(): ConsoleOptionParser
When describing columns you can use the following syntax:
-{name}:{primary}{type}{nullable}[{length}]:{index}:{indexName}
+{name}:{type}{nullable}[{length}]:default[{value}]:{index}:{indexName}
All sections other than name are optional.
@@ -182,6 +182,9 @@ public function getOptionParser(): ConsoleOptionParser
* The ? value indicates if a column is nullable.
e.g. role:string?.
* Length option must be enclosed in [], for example: name:string?[100].
+* The default[value] option sets a default value for the column.
+ Supports booleans (true/false), integers, floats, strings, and null.
+ e.g. active:boolean:default[true], count:integer:default[0].
* The index attribute can define the column as having a unique
key with unique or a primary key with primary.
* Use references type to create a foreign key constraint.
@@ -214,6 +217,12 @@ public function getOptionParser(): ConsoleOptionParser
Create a migration that adds a foreign key column (category_id) to the articles
table referencing the categories table.
+bin/cake bake migration AddActiveToUsers active:boolean:default[true]
+Create a migration that adds an active column with a default value of true.
+
+bin/cake bake migration AddCountToProducts count:integer:default[0]:unique
+Create a migration that adds a count column with default 0 and a unique index.
+
Migration Styles
You can generate migrations in different styles:
diff --git a/src/Util/ColumnParser.php b/src/Util/ColumnParser.php
index fa8acd64..e0c7ca1e 100644
--- a/src/Util/ColumnParser.php
+++ b/src/Util/ColumnParser.php
@@ -28,6 +28,7 @@ class ColumnParser
(?:,(?:[0-9]|[1-9][0-9]+))?
\])?
))?
+ (?::default\[([^\]]+)\])?
(?::(\w+))?
(?::(\w+))?
$
@@ -54,7 +55,8 @@ public function parseFields(array $arguments): array
preg_match($this->regexpParseColumn, $field, $matches);
$field = $matches[1];
$type = Hash::get($matches, 2, '');
- $indexType = Hash::get($matches, 3);
+ $defaultValue = Hash::get($matches, 3);
+ $indexType = Hash::get($matches, 4);
$typeIsPk = in_array($type, ['primary', 'primary_key'], true);
$isPrimaryKey = false;
@@ -80,7 +82,7 @@ public function parseFields(array $arguments): array
'columnType' => $type,
'options' => [
'null' => $nullable,
- 'default' => null,
+ 'default' => $this->parseDefaultValue($defaultValue, $type ?? 'string'),
],
];
@@ -114,8 +116,8 @@ public function parseIndexes(array $arguments): array
preg_match($this->regexpParseColumn, $field, $matches);
$field = $matches[1];
$type = Hash::get($matches, 2);
- $indexType = Hash::get($matches, 3);
- $indexName = Hash::get($matches, 4);
+ $indexType = Hash::get($matches, 4);
+ $indexName = Hash::get($matches, 5);
// Skip references - they create foreign keys, not indexes
if ($type && str_starts_with($type, 'references')) {
@@ -168,7 +170,7 @@ public function parsePrimaryKey(array $arguments): array
preg_match($this->regexpParseColumn, $field, $matches);
$field = $matches[1];
$type = Hash::get($matches, 2);
- $indexType = Hash::get($matches, 3);
+ $indexType = Hash::get($matches, 4);
if (
in_array($type, ['primary', 'primary_key'], true)
@@ -196,8 +198,8 @@ public function parseForeignKeys(array $arguments): array
preg_match($this->regexpParseColumn, $field, $matches);
$fieldName = $matches[1];
$type = Hash::get($matches, 2, '');
- $indexType = Hash::get($matches, 3);
- $indexName = Hash::get($matches, 4);
+ $indexType = Hash::get($matches, 4);
+ $indexName = Hash::get($matches, 5);
// Check if type is 'references' or 'references?'
$isReference = str_starts_with($type, 'references');
@@ -250,17 +252,20 @@ public function validArguments(array $arguments): array
*
* @param string $field Name of field
* @param string|null $type User-specified type
- * @return array First value is the field type, second value is the field length. If no length
+ * @return array{0: string|null, 1: int|array|null} First value is the field type, second value is the field length. If no length
* can be extracted, null is returned for the second value
*/
public function getTypeAndLength(string $field, ?string $type): array
{
if ($type && preg_match($this->regexpParseField, $type, $matches)) {
- if (str_contains($matches[2], ',')) {
- $matches[2] = explode(',', $matches[2]);
+ $length = $matches[2];
+ if (str_contains($length, ',')) {
+ $length = array_map('intval', explode(',', $length));
+ } else {
+ $length = (int)$length;
}
- return [$matches[1], $matches[2]];
+ return [$matches[1], $length];
}
/** @var string $fieldType */
@@ -352,4 +357,61 @@ public function getIndexName(string $field, ?string $indexType, ?string $indexNa
return $indexName;
}
+
+ /**
+ * Parses a default value string into the appropriate PHP type.
+ *
+ * Supports:
+ * - Booleans: true, false
+ * - Null: null, NULL
+ * - Integers: 123, -123
+ * - Floats: 1.5, -1.5
+ * - Strings: 'hello' (quoted) or unquoted values
+ *
+ * @param string|null $value The raw default value from the command line
+ * @param string $columnType The column type to help with type coercion
+ * @return string|int|float|bool|null The parsed default value
+ */
+ public function parseDefaultValue(?string $value, string $columnType): string|int|float|bool|null
+ {
+ if ($value === null || $value === '') {
+ return null;
+ }
+
+ $lowerValue = strtolower($value);
+
+ // Handle null
+ if ($lowerValue === 'null') {
+ return null;
+ }
+
+ // Handle booleans
+ if ($lowerValue === 'true') {
+ return true;
+ }
+ if ($lowerValue === 'false') {
+ return false;
+ }
+
+ // Handle quoted strings - strip quotes
+ if (
+ (str_starts_with($value, "'") && str_ends_with($value, "'")) ||
+ (str_starts_with($value, '"') && str_ends_with($value, '"'))
+ ) {
+ return substr($value, 1, -1);
+ }
+
+ // Handle integers
+ if (preg_match('/^-?[0-9]+$/', $value)) {
+ return (int)$value;
+ }
+
+ // Handle floats
+ if (preg_match('/^-?[0-9]+\.[0-9]+$/', $value)) {
+ return (float)$value;
+ }
+
+ // Return as-is for SQL expressions like CURRENT_TIMESTAMP
+ return $value;
+ }
}
diff --git a/tests/TestCase/Util/ColumnParserTest.php b/tests/TestCase/Util/ColumnParserTest.php
index 1a31d671..d56d0616 100644
--- a/tests/TestCase/Util/ColumnParserTest.php
+++ b/tests/TestCase/Util/ColumnParserTest.php
@@ -346,6 +346,36 @@ public function testGetTypeAndLength()
$this->assertEquals(['decimal', [10, 6]], $this->columnParser->getTypeAndLength('latitude', 'decimal[10,6]'));
}
+ public function testGetTypeAndLengthReturnsIntegerTypes()
+ {
+ // Test that lengths are returned as integers, not strings
+ [, $length] = $this->columnParser->getTypeAndLength('name', 'string[128]');
+ $this->assertIsInt($length);
+ $this->assertSame(128, $length);
+
+ [, $length] = $this->columnParser->getTypeAndLength('count', 'integer[9]');
+ $this->assertIsInt($length);
+ $this->assertSame(9, $length);
+
+ // Test that precision/scale arrays contain integers
+ [, $length] = $this->columnParser->getTypeAndLength('amount', 'decimal[10,6]');
+ $this->assertIsArray($length);
+ $this->assertCount(2, $length);
+ $this->assertIsInt($length[0]);
+ $this->assertIsInt($length[1]);
+ $this->assertSame(10, $length[0]);
+ $this->assertSame(6, $length[1]);
+
+ // Test default lengths are also integers
+ [, $length] = $this->columnParser->getTypeAndLength('name', 'string');
+ $this->assertIsInt($length);
+ $this->assertSame(255, $length);
+
+ [, $length] = $this->columnParser->getTypeAndLength('id', 'integer');
+ $this->assertIsInt($length);
+ $this->assertSame(11, $length);
+ }
+
public function testGetLength()
{
$this->assertSame(255, $this->columnParser->getLength('string'));
@@ -413,6 +443,190 @@ public function testParseFieldsWithReferences()
$this->assertEquals($expected, $actual);
}
+ public function testParseFieldsWithDefaultValues()
+ {
+ // Test boolean default true
+ $expected = [
+ 'active' => [
+ 'columnType' => 'boolean',
+ 'options' => [
+ 'null' => false,
+ 'default' => true,
+ ],
+ ],
+ ];
+ $actual = $this->columnParser->parseFields(['active:boolean:default[true]']);
+ $this->assertEquals($expected, $actual);
+
+ // Test boolean default false
+ $expected = [
+ 'skip_updates' => [
+ 'columnType' => 'boolean',
+ 'options' => [
+ 'null' => false,
+ 'default' => false,
+ ],
+ ],
+ ];
+ $actual = $this->columnParser->parseFields(['skip_updates:boolean:default[false]']);
+ $this->assertEquals($expected, $actual);
+
+ // Test integer default
+ $expected = [
+ 'count' => [
+ 'columnType' => 'integer',
+ 'options' => [
+ 'null' => false,
+ 'default' => 0,
+ 'limit' => 11,
+ ],
+ ],
+ ];
+ $actual = $this->columnParser->parseFields(['count:integer:default[0]']);
+ $this->assertEquals($expected, $actual);
+
+ // Test string default with quotes
+ $expected = [
+ 'status' => [
+ 'columnType' => 'string',
+ 'options' => [
+ 'null' => false,
+ 'default' => 'pending',
+ 'limit' => 255,
+ ],
+ ],
+ ];
+ $actual = $this->columnParser->parseFields(["status:string:default['pending']"]);
+ $this->assertEquals($expected, $actual);
+
+ // Test nullable with default
+ $expected = [
+ 'role' => [
+ 'columnType' => 'string',
+ 'options' => [
+ 'null' => true,
+ 'default' => 'user',
+ 'limit' => 255,
+ ],
+ ],
+ ];
+ $actual = $this->columnParser->parseFields(["role:string?:default['user']"]);
+ $this->assertEquals($expected, $actual);
+
+ // Test default with index
+ $expected = [
+ 'email' => [
+ 'columnType' => 'string',
+ 'options' => [
+ 'null' => false,
+ 'default' => null,
+ 'limit' => 255,
+ ],
+ ],
+ ];
+ $actual = $this->columnParser->parseFields(['email:string:default[null]:unique']);
+ $this->assertEquals($expected, $actual);
+
+ // Test float default
+ $expected = [
+ 'rate' => [
+ 'columnType' => 'decimal',
+ 'options' => [
+ 'null' => false,
+ 'default' => 1.5,
+ 'precision' => 10,
+ 'scale' => 6,
+ ],
+ ],
+ ];
+ $actual = $this->columnParser->parseFields(['rate:decimal:default[1.5]']);
+ $this->assertEquals($expected, $actual);
+
+ // Test length with default
+ $expected = [
+ 'code' => [
+ 'columnType' => 'string',
+ 'options' => [
+ 'null' => false,
+ 'default' => 'ABC',
+ 'limit' => 10,
+ ],
+ ],
+ ];
+ $actual = $this->columnParser->parseFields(["code:string[10]:default['ABC']"]);
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testParseDefaultValue()
+ {
+ // Test null and empty values
+ $this->assertNull($this->columnParser->parseDefaultValue(null, 'string'));
+ $this->assertNull($this->columnParser->parseDefaultValue('', 'string'));
+ $this->assertNull($this->columnParser->parseDefaultValue('null', 'string'));
+ $this->assertNull($this->columnParser->parseDefaultValue('NULL', 'string'));
+
+ // Test boolean values
+ $this->assertTrue($this->columnParser->parseDefaultValue('true', 'boolean'));
+ $this->assertTrue($this->columnParser->parseDefaultValue('TRUE', 'boolean'));
+ $this->assertFalse($this->columnParser->parseDefaultValue('false', 'boolean'));
+ $this->assertFalse($this->columnParser->parseDefaultValue('FALSE', 'boolean'));
+
+ // Test integer values
+ $this->assertSame(0, $this->columnParser->parseDefaultValue('0', 'integer'));
+ $this->assertSame(123, $this->columnParser->parseDefaultValue('123', 'integer'));
+ $this->assertSame(-456, $this->columnParser->parseDefaultValue('-456', 'integer'));
+
+ // Test float values
+ $this->assertSame(1.5, $this->columnParser->parseDefaultValue('1.5', 'decimal'));
+ $this->assertSame(-2.75, $this->columnParser->parseDefaultValue('-2.75', 'decimal'));
+
+ // Test quoted strings
+ $this->assertSame('hello', $this->columnParser->parseDefaultValue("'hello'", 'string'));
+ $this->assertSame('world', $this->columnParser->parseDefaultValue('"world"', 'string'));
+
+ // Test SQL expressions (returned as-is)
+ $this->assertSame('CURRENT_TIMESTAMP', $this->columnParser->parseDefaultValue('CURRENT_TIMESTAMP', 'datetime'));
+ }
+
+ public function testParseIndexesWithDefaultValues()
+ {
+ // Ensure indexes still work with default values in the definition
+ $expected = [
+ 'UNIQUE_EMAIL' => [
+ 'columns' => ['email'],
+ 'options' => ['unique' => true, 'name' => 'UNIQUE_EMAIL'],
+ ],
+ ];
+ $actual = $this->columnParser->parseIndexes(['email:string:default[null]:unique']);
+ $this->assertEquals($expected, $actual);
+
+ // Test with custom index name
+ $expected = [
+ 'IDX_COUNT' => [
+ 'columns' => ['count'],
+ 'options' => ['unique' => false, 'name' => 'IDX_COUNT'],
+ ],
+ ];
+ $actual = $this->columnParser->parseIndexes(['count:integer:default[0]:index:IDX_COUNT']);
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testValidArgumentsWithDefaultValues()
+ {
+ $this->assertEquals(
+ ['active:boolean:default[true]'],
+ $this->columnParser->validArguments(['active:boolean:default[true]']),
+ );
+ $this->assertEquals(
+ ['count:integer:default[0]:unique'],
+ $this->columnParser->validArguments(['count:integer:default[0]:unique']),
+ );
+ $this->assertEquals(
+ ["status:string:default['pending']:index:IDX_STATUS"],
+ $this->columnParser->validArguments(["status:string:default['pending']:index:IDX_STATUS"]),
+ );
+ }
+
public function testParseForeignKeys()
{
// Test basic reference - infer table name from field