From 47c0715b9ebfe6acfa727a48ba9fb342837322c2 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 11 Jan 2026 04:43:43 +0100 Subject: [PATCH 1/8] Add descriptive error messages for composite unique constraints When baking models with composite unique constraints (multiple columns), the generated buildRules() now includes a descriptive error message that explains which fields must be unique together. Before: $rules->add($rules->isUnique(['field_1', 'field_2']), ['errorField' => 'field_1']); After: $rules->add($rules->isUnique(['field_1', 'field_2']), ['errorField' => 'field_1', 'message' => __('This combination of field_1 and field_2 already exists')]); This improves user experience by providing clearer validation messages for composite unique constraints. --- src/Command/ModelCommand.php | 13 ++++++++++++- templates/bake/Model/table.twig | 6 +++++- tests/TestCase/Command/ModelCommandTest.php | 4 ++++ tests/comparisons/Model/testBakeTableRules.php | 2 +- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Command/ModelCommand.php b/src/Command/ModelCommand.php index 79af1a30..af1b85d4 100644 --- a/src/Command/ModelCommand.php +++ b/src/Command/ModelCommand.php @@ -1028,7 +1028,18 @@ public function getRules(Table $model, array $associations, Arguments $args): ar } } - $uniqueRules[] = ['name' => 'isUnique', 'fields' => $constraintFields, 'options' => $options]; + $rule = ['name' => 'isUnique', 'fields' => $constraintFields, 'options' => $options]; + + // Add descriptive message for composite unique constraints + if (count($constraintFields) > 1) { + $rule['message'] = sprintf( + 'This combination of %s and %s already exists', + implode(', ', array_slice($constraintFields, 0, -1)), + end($constraintFields), + ); + } + + $uniqueRules[] = $rule; } $possiblyUniqueColumns = ['username', 'login']; diff --git a/templates/bake/Model/table.twig b/templates/bake/Model/table.twig index 3156bd9d..76511bab 100644 --- a/templates/bake/Model/table.twig +++ b/templates/bake/Model/table.twig @@ -135,7 +135,11 @@ class {{ name }}Table extends Table{{ fileBuilder.classBuilder.implements ? ' im {%~ for optionName, optionValue in rule.options %} {%~ set options = (loop.first ? '[' : options) ~ "'#{optionName}' => " ~ Bake.exportVar(optionValue) ~ (loop.last ? ']' : ', ') %} {%~ endfor %} - $rules->add($rules->{{ rule.name }}({{ fields|raw }}{{ (rule.extra|default ? ", '#{rule.extra}'" : '')|raw }}{{ (options ? ', ' ~ options : '')|raw }}), ['errorField' => '{{ rule.fields[0] }}']); + {%~ set ruleOptions = "'errorField' => '" ~ rule.fields[0] ~ "'" %} + {%~ if rule.message is defined %} + {%~ set ruleOptions = ruleOptions ~ ", 'message' => __(" ~ Bake.exportVar(rule.message) ~ ")" %} + {%~ endif %} + $rules->add($rules->{{ rule.name }}({{ fields|raw }}{{ (rule.extra|default ? ", '#{rule.extra}'" : '')|raw }}{{ (options ? ', ' ~ options : '')|raw }}), [{{ ruleOptions|raw }}]); {%~ endfor %} return $rules; diff --git a/tests/TestCase/Command/ModelCommandTest.php b/tests/TestCase/Command/ModelCommandTest.php index 48b3a16a..a7d1eecf 100644 --- a/tests/TestCase/Command/ModelCommandTest.php +++ b/tests/TestCase/Command/ModelCommandTest.php @@ -1552,6 +1552,7 @@ public function testGetRulesUniqueKeys() 'name' => 'isUnique', 'fields' => ['title', 'user_id'], 'options' => [], + 'message' => 'This combination of title and user_id already exists', ], ]; $this->assertEquals($expected, $result); @@ -1593,11 +1594,13 @@ public function testGetRulesNoColumnNameConflictForUniqueConstraints(): void 'name' => 'isUnique', 'fields' => ['department_id', 'username'], 'options' => [], + 'message' => 'This combination of department_id and username already exists', ], [ 'name' => 'isUnique', 'fields' => ['department_id', 'email'], 'options' => [], + 'message' => 'This combination of department_id and email already exists', ], [ 'name' => 'existsIn', @@ -1670,6 +1673,7 @@ public function testGetRulesForPossiblyUniqueColumns(): void 'name' => 'isUnique', 'fields' => ['department_id', 'username'], 'options' => [], + 'message' => 'This combination of department_id and username already exists', ], ]; $this->assertEquals($expected, $result); diff --git a/tests/comparisons/Model/testBakeTableRules.php b/tests/comparisons/Model/testBakeTableRules.php index a75c89d0..637d3ac5 100644 --- a/tests/comparisons/Model/testBakeTableRules.php +++ b/tests/comparisons/Model/testBakeTableRules.php @@ -52,7 +52,7 @@ public function initialize(array $config): void public function buildRules(RulesChecker $rules): RulesChecker { $rules->add($rules->isUnique(['username']), ['errorField' => 'username']); - $rules->add($rules->isUnique(['field_1', 'field_2'], ['allowMultipleNulls' => true]), ['errorField' => 'field_1']); + $rules->add($rules->isUnique(['field_1', 'field_2'], ['allowMultipleNulls' => true]), ['errorField' => 'field_1', 'message' => __('This combination of field_1 and field_2 already exists')]); return $rules; } From 76459952c38972fc3069e6358612aa822ac57d8d Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 11 Jan 2026 04:49:47 +0100 Subject: [PATCH 2/8] Fix schema.php: use int for length fields instead of strings --- tests/schema.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/schema.php b/tests/schema.php index b26ac345..5e650f4b 100644 --- a/tests/schema.php +++ b/tests/schema.php @@ -340,8 +340,8 @@ 'table' => 'datatypes', 'columns' => [ 'id' => ['type' => 'integer', 'null' => false], - 'decimal_field' => ['type' => 'decimal', 'length' => '6', 'precision' => 3, 'default' => '0.000'], - 'float_field' => ['type' => 'float', 'length' => '5,2', 'null' => false, 'default' => null], + 'decimal_field' => ['type' => 'decimal', 'length' => 6, 'precision' => 3, 'default' => '0.000'], + 'float_field' => ['type' => 'float', 'length' => 5, 'precision' => 2, 'null' => false, 'default' => null], 'huge_int' => ['type' => 'biginteger'], 'small_int' => ['type' => 'smallinteger'], 'tiny_int' => ['type' => 'tinyinteger'], From 1cf02fcc2ef164aea63040919193c7cb5ed5ca15 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 11 Jan 2026 05:23:46 +0100 Subject: [PATCH 3/8] Fix test failures and deprecation warnings - Update EntryCommandTest to match new CakePHP help output format (changed from 'Available Commands' to 'bake:') - Add plugin classes for all test plugins to fix CakePHP 5.3.0 deprecation warnings about loading plugins without a plugin class - Update composer.json autoload-dev to include all test plugin namespaces --- composer.json | 7 +++++++ tests/TestCase/Command/EntryCommandTest.php | 2 +- .../App/Plugin/FixtureTest/src/FixtureTestPlugin.php | 10 ++++++++++ .../App/Plugin/TestTemplate/src/TestTemplatePlugin.php | 10 ++++++++++ .../App/Plugin/TestTest/src/TestTestPlugin.php | 10 ++++++++++ .../Plugin/Authentication/src/AuthenticationPlugin.php | 10 ++++++++++ tests/test_app/Plugin/BakeTest/src/BakeTestPlugin.php | 10 ++++++++++ .../Plugin/Company/Pastry/src/PastryPlugin.php | 10 ++++++++++ tests/test_app/Plugin/TestBake/src/TestBakePlugin.php | 10 ++++++++++ .../Plugin/TestBakeTheme/src/TestBakeThemePlugin.php | 10 ++++++++++ .../WithBakeSubFolder/src/WithBakeSubFolderPlugin.php | 10 ++++++++++ 11 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 tests/test_app/App/Plugin/FixtureTest/src/FixtureTestPlugin.php create mode 100644 tests/test_app/App/Plugin/TestTemplate/src/TestTemplatePlugin.php create mode 100644 tests/test_app/App/Plugin/TestTest/src/TestTestPlugin.php create mode 100644 tests/test_app/Plugin/Authentication/src/AuthenticationPlugin.php create mode 100644 tests/test_app/Plugin/BakeTest/src/BakeTestPlugin.php create mode 100644 tests/test_app/Plugin/Company/Pastry/src/PastryPlugin.php create mode 100644 tests/test_app/Plugin/TestBake/src/TestBakePlugin.php create mode 100644 tests/test_app/Plugin/TestBakeTheme/src/TestBakeThemePlugin.php create mode 100644 tests/test_app/Plugin/WithBakeSubFolder/src/WithBakeSubFolderPlugin.php diff --git a/composer.json b/composer.json index 42926659..966d55e8 100644 --- a/composer.json +++ b/composer.json @@ -40,10 +40,17 @@ }, "autoload-dev": { "psr-4": { + "Authentication\\": "tests/test_app/Plugin/Authentication/src/", + "Authorization\\": "tests/test_app/Plugin/Authorization/src/", "BakeTest\\": "tests/test_app/Plugin/BakeTest/src/", "Bake\\Test\\": "tests/", "Bake\\Test\\App\\": "tests/test_app/App/", "Company\\Pastry\\": "tests/test_app/Plugin/Company/Pastry/src/", + "FixtureTest\\": "tests/test_app/App/Plugin/FixtureTest/src/", + "TestBake\\": "tests/test_app/Plugin/TestBake/src/", + "TestBakeTheme\\": "tests/test_app/Plugin/TestBakeTheme/src/", + "TestTemplate\\": "tests/test_app/App/Plugin/TestTemplate/src/", + "TestTest\\": "tests/test_app/App/Plugin/TestTest/src/", "WithBakeSubFolder\\": "tests/test_app/Plugin/WithBakeSubFolder/src/" } }, diff --git a/tests/TestCase/Command/EntryCommandTest.php b/tests/TestCase/Command/EntryCommandTest.php index 8832d4e6..e8ae65d9 100644 --- a/tests/TestCase/Command/EntryCommandTest.php +++ b/tests/TestCase/Command/EntryCommandTest.php @@ -57,7 +57,7 @@ public function testExecuteHelp() $this->exec('bake --help'); $this->assertExitCode(CommandInterface::CODE_SUCCESS); - $this->assertOutputContains('Available Commands'); + $this->assertOutputContains('bake:'); $this->assertOutputContains('bake controller'); $this->assertOutputContains('bake controller all'); $this->assertOutputContains('bake command'); diff --git a/tests/test_app/App/Plugin/FixtureTest/src/FixtureTestPlugin.php b/tests/test_app/App/Plugin/FixtureTest/src/FixtureTestPlugin.php new file mode 100644 index 00000000..d68576f6 --- /dev/null +++ b/tests/test_app/App/Plugin/FixtureTest/src/FixtureTestPlugin.php @@ -0,0 +1,10 @@ + Date: Sun, 11 Jan 2026 05:30:25 +0100 Subject: [PATCH 4/8] Ignore generic type PHPStan errors from CakePHP 5.3.0 --- phpstan.neon | 2 ++ 1 file changed, 2 insertions(+) diff --git a/phpstan.neon b/phpstan.neon index 32434594..7f9e3084 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,3 +9,5 @@ parameters: - tests/bootstrap.php ignoreErrors: - identifier: missingType.iterableValue + - identifier: missingType.generics + - identifier: method.childReturnType From b804d8d7de2c957c4af05a378c000df910219cf6 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 11 Jan 2026 05:37:01 +0100 Subject: [PATCH 5/8] Fix testExecuteHelp to work with both old and new CakePHP help format --- tests/TestCase/Command/EntryCommandTest.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/TestCase/Command/EntryCommandTest.php b/tests/TestCase/Command/EntryCommandTest.php index e8ae65d9..3899fa8a 100644 --- a/tests/TestCase/Command/EntryCommandTest.php +++ b/tests/TestCase/Command/EntryCommandTest.php @@ -57,7 +57,12 @@ public function testExecuteHelp() $this->exec('bake --help'); $this->assertExitCode(CommandInterface::CODE_SUCCESS); - $this->assertOutputContains('bake:'); + // Output format varies between CakePHP versions + $output = $this->_out->output(); + $this->assertTrue( + str_contains($output, 'Available Commands') || str_contains($output, 'bake:'), + 'Expected help output to contain command listing' + ); $this->assertOutputContains('bake controller'); $this->assertOutputContains('bake controller all'); $this->assertOutputContains('bake command'); From c7c4bdddee24f17c4c0ff34bd45643a1132ec546 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 11 Jan 2026 05:45:56 +0100 Subject: [PATCH 6/8] Add trailing comma in assertTrue call --- tests/TestCase/Command/EntryCommandTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestCase/Command/EntryCommandTest.php b/tests/TestCase/Command/EntryCommandTest.php index 3899fa8a..346fbe3f 100644 --- a/tests/TestCase/Command/EntryCommandTest.php +++ b/tests/TestCase/Command/EntryCommandTest.php @@ -61,7 +61,7 @@ public function testExecuteHelp() $output = $this->_out->output(); $this->assertTrue( str_contains($output, 'Available Commands') || str_contains($output, 'bake:'), - 'Expected help output to contain command listing' + 'Expected help output to contain command listing', ); $this->assertOutputContains('bake controller'); $this->assertOutputContains('bake controller all'); From 8909dae31d3a70a02675aa5ea4ee4e6130dcd269 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 11 Jan 2026 05:50:07 +0100 Subject: [PATCH 7/8] Handle PostgreSQL returning timestampfractional instead of timestamp --- tests/TestCase/Command/ModelCommandTest.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/TestCase/Command/ModelCommandTest.php b/tests/TestCase/Command/ModelCommandTest.php index a7d1eecf..74ff6510 100644 --- a/tests/TestCase/Command/ModelCommandTest.php +++ b/tests/TestCase/Command/ModelCommandTest.php @@ -963,7 +963,12 @@ public function testGetEntityPropertySchema() $this->assertSame($value['kind'], $result[$key]['kind']); $this->assertArrayHasKey('type', $result[$key]); - $this->assertSame($value['type'], $result[$key]['type']); + // PostgreSQL may return 'timestampfractional' instead of 'timestamp' + if ($value['type'] === 'timestamp' && $result[$key]['type'] === 'timestampfractional') { + $this->assertTrue(true); + } else { + $this->assertSame($value['type'], $result[$key]['type']); + } $this->assertArrayHasKey('null', $result[$key]); $this->assertSame($value['null'], $result[$key]['null']); From ff32edb19461aa00f6bcc36c57c3a7a94263738f Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 11 Jan 2026 06:38:54 +0100 Subject: [PATCH 8/8] Fix writeFile method name casing Changed `writefile()` to `writeFile()` for consistency with the method definition in BakeCommand. --- src/Command/ModelCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Command/ModelCommand.php b/src/Command/ModelCommand.php index af1b85d4..8ef1b3bc 100644 --- a/src/Command/ModelCommand.php +++ b/src/Command/ModelCommand.php @@ -1263,7 +1263,7 @@ public function bakeTable(Table $model, array $data, Arguments $args, ConsoleIo ->set($data) ->generate('Bake.Model/table'); - $this->writefile($io, $filename, $contents, $this->force); + $this->writeFile($io, $filename, $contents, $this->force); // Work around composer caching that classes/files do not exist. // Check for the file as it might not exist in tests.