From 07a460ae8678ec8c422e278bd2158fb4543f1ed1 Mon Sep 17 00:00:00 2001 From: ndm2 Date: Fri, 22 Dec 2023 17:01:14 +0100 Subject: [PATCH 1/5] Fix unique constraints getting lost. When using the first column of the constraints and foreign keys to index the rules array, constraints and foreign keys that share the same first column in the composite constraint will overwrite previously set rules. refs #957 --- src/Command/ModelCommand.php | 49 ++++-- templates/bake/Model/table.twig | 6 +- tests/TestCase/Command/ModelCommandTest.php | 140 ++++++++++++++++-- ...iationDetectionCategoriesProductsTable.php | 4 +- ...sociationDetectionProductVersionsTable.php | 2 +- ...ionDetectionProductVersionsTableSigned.php | 2 +- .../comparisons/Model/testBakeTableConfig.php | 2 +- .../Model/testBakeTableWithCounterCache.php | 2 +- .../Model/testBakeUpdateTableNoFile.php | 2 +- 9 files changed, 174 insertions(+), 35 deletions(-) diff --git a/src/Command/ModelCommand.php b/src/Command/ModelCommand.php index 2b006f50..05ebce63 100644 --- a/src/Command/ModelCommand.php +++ b/src/Command/ModelCommand.php @@ -984,22 +984,14 @@ public function getRules(Table $model, array $associations, Arguments $args): ar return []; } $schema = $model->getSchema(); - $fields = $schema->columns(); - if (empty($fields)) { + $schemaFields = $schema->columns(); + if (empty($schemaFields)) { return []; } - $uniqueColumns = ['username', 'login']; - if (in_array($model->getAlias(), ['Users', 'Accounts'])) { - $uniqueColumns[] = 'email'; - } + $uniqueRules = []; + $uniqueConstraintsColumns = []; - $rules = []; - foreach ($fields as $fieldName) { - if (in_array($fieldName, $uniqueColumns, true)) { - $rules[$fieldName] = ['name' => 'isUnique', 'fields' => [$fieldName], 'options' => []]; - } - } foreach ($schema->constraints() as $name) { $constraint = $schema->getConstraint($name); if ($constraint['type'] !== TableSchema::CONSTRAINT_UNIQUE) { @@ -1007,8 +999,11 @@ public function getRules(Table $model, array $associations, Arguments $args): ar } $options = []; - $fields = $constraint['columns']; - foreach ($fields as $field) { + /** @var array $constraintFields */ + $constraintFields = $constraint['columns']; + $uniqueConstraintsColumns = [...$uniqueConstraintsColumns, ...$constraintFields]; + + foreach ($constraintFields as $field) { if ($schema->isNullable($field)) { $allowMultiple = !ConnectionManager::get($this->connection)->getDriver() instanceof Sqlserver; $options['allowMultipleNulls'] = $allowMultiple; @@ -1016,15 +1011,37 @@ public function getRules(Table $model, array $associations, Arguments $args): ar } } - $rules[$constraint['columns'][0]] = ['name' => 'isUnique', 'fields' => $fields, 'options' => $options]; + $uniqueRules[] = ['name' => 'isUnique', 'fields' => $constraintFields, 'options' => $options]; } + $possiblyUniqueColumns = ['username', 'login']; + if (in_array($model->getAlias(), ['Users', 'Accounts'])) { + $possiblyUniqueColumns[] = 'email'; + } + + $possiblyUniqueRules = []; + foreach ($schemaFields as $field) { + if ( + !in_array($field, $uniqueConstraintsColumns, true) && + in_array($field, $possiblyUniqueColumns, true) + ) { + $possiblyUniqueRules[] = ['name' => 'isUnique', 'fields' => [$field], 'options' => []]; + } + } + + $rules = [...$possiblyUniqueRules, ...$uniqueRules]; + if (empty($associations['belongsTo'])) { return $rules; } foreach ($associations['belongsTo'] as $assoc) { - $rules[$assoc['foreignKey']] = ['name' => 'existsIn', 'extra' => $assoc['alias'], 'options' => []]; + $rules[] = [ + 'name' => 'existsIn', + 'fields' => (array)$assoc['foreignKey'], + 'extra' => $assoc['alias'], + 'options' => [], + ]; } return $rules; diff --git a/templates/bake/Model/table.twig b/templates/bake/Model/table.twig index 5a1b9620..41a1eb2e 100644 --- a/templates/bake/Model/table.twig +++ b/templates/bake/Model/table.twig @@ -128,13 +128,13 @@ class {{ name }}Table extends Table{{ fileBuilder.classBuilder.implements ? ' im */ public function buildRules(RulesChecker $rules): RulesChecker { -{% for field, rule in rulesChecker %} -{% set fields = rule.fields is defined ? Bake.exportArray(rule.fields) : Bake.exportVar(field) %} +{% for rule in rulesChecker %} +{% set fields = Bake.exportArray(rule.fields) %} {% set options = '' %} {% 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' => '{{ field }}']); + $rules->add($rules->{{ rule.name }}({{ fields|raw }}{{ (rule.extra|default ? ", '#{rule.extra}'" : '')|raw }}{{ (options ? ', ' ~ options : '')|raw }}), ['errorField' => '{{ rule.fields[0] }}']); {% endfor %} return $rules; diff --git a/tests/TestCase/Command/ModelCommandTest.php b/tests/TestCase/Command/ModelCommandTest.php index 3e5f8bd9..4a3a8d3b 100644 --- a/tests/TestCase/Command/ModelCommandTest.php +++ b/tests/TestCase/Command/ModelCommandTest.php @@ -1368,7 +1368,7 @@ public function testGetRules() ], [ 'alias' => 'Sites', - 'foreignKey' => 'site_id', + 'foreignKey' => ['site_id1', 'site_id2'], ], ], 'hasMany' => [ @@ -1382,18 +1382,20 @@ public function testGetRules() $args = new Arguments([], [], []); $result = $command->getRules($model, $associations, $args); $expected = [ - 'username' => [ + [ 'name' => 'isUnique', 'fields' => ['username'], 'options' => [], ], - 'country_id' => [ + [ 'name' => 'existsIn', + 'fields' => ['country_id'], 'extra' => 'Countries', 'options' => [], ], - 'site_id' => [ + [ 'name' => 'existsIn', + 'fields' => ['site_id1', 'site_id2'], 'extra' => 'Sites', 'options' => [], ], @@ -1404,9 +1406,6 @@ public function testGetRules() /** * Tests the getRules with unique keys. * - * Multi-column constraints are ignored as they would - * require a break in compatibility. - * * @return void */ public function testGetRulesUniqueKeys() @@ -1416,7 +1415,7 @@ public function testGetRulesUniqueKeys() 'type' => 'unique', 'columns' => ['title'], ]); - $model->getSchema()->addConstraint('ignored_constraint', [ + $model->getSchema()->addConstraint('unique_composite', [ 'type' => 'unique', 'columns' => ['title', 'user_id'], ]); @@ -1425,7 +1424,12 @@ public function testGetRulesUniqueKeys() $args = new Arguments([], [], []); $result = $command->getRules($model, [], $args); $expected = [ - 'title' => [ + [ + 'name' => 'isUnique', + 'fields' => ['title'], + 'options' => [], + ], + [ 'name' => 'isUnique', 'fields' => ['title', 'user_id'], 'options' => [], @@ -1434,6 +1438,124 @@ public function testGetRulesUniqueKeys() $this->assertEquals($expected, $result); } + /** + * Tests that there are no conflicts between neither multiple constraints, + * nor with foreign keys that share one or more identical column. + */ + public function testGetRulesNoColumnNameConflictForUniqueConstraints(): void + { + $model = $this->getTableLocator()->get('Users'); + $model->setSchema([ + 'department_id' => ['type' => 'integer', 'null' => false], + 'username' => ['type' => 'string', 'null' => false], + 'email' => ['type' => 'string', 'null' => false], + ]); + + $model->getSchema()->addConstraint('unique_composite_1', [ + 'type' => 'unique', + 'columns' => ['department_id', 'username'], + ]); + $model->getSchema()->addConstraint('unique_composite_2', [ + 'type' => 'unique', + 'columns' => ['department_id', 'email'], + ]); + + $command = new ModelCommand(); + $args = new Arguments([], [], []); + $associations = [ + 'belongsTo' => [ + ['alias' => 'Departments', 'foreignKey' => 'department_id'], + ], + ]; + + $result = $command->getRules($model, $associations, $args); + $expected = [ + [ + 'name' => 'isUnique', + 'fields' => ['department_id', 'username'], + 'options' => [], + ], + [ + 'name' => 'isUnique', + 'fields' => ['department_id', 'email'], + 'options' => [], + ], + [ + 'name' => 'existsIn', + 'fields' => ['department_id'], + 'extra' => 'Departments', + 'options' => [], + ], + ]; + $this->assertEquals($expected, $result); + } + + /** + * Tests generating unique rules for possibly unique columns based on + * column names instead of on actual unique constraints. + */ + public function testGetRulesForPossiblyUniqueColumns(): void + { + $model = $this->getTableLocator()->get('Users'); + $model->setSchema([ + 'department_id' => ['type' => 'integer', 'null' => false], + 'username' => ['type' => 'string', 'null' => false], + 'login' => ['type' => 'string', 'null' => false], + 'email' => ['type' => 'string', 'null' => false], + ]); + + $command = new ModelCommand(); + $args = new Arguments([], [], []); + $result = $command->getRules($model, [], $args); + $expected = [ + [ + 'name' => 'isUnique', + 'fields' => ['username'], + 'options' => [], + ], + [ + 'name' => 'isUnique', + 'fields' => ['login'], + 'options' => [], + ], + [ + 'name' => 'isUnique', + 'fields' => ['email'], + 'options' => [], + ], + ]; + $this->assertEquals($expected, $result); + + // possibly unique columns should not cause additional rules + // to be generated in case the column is already present in + // an actual unique constraint + + $model->getSchema()->addConstraint('unique_composite', [ + 'type' => 'unique', + 'columns' => ['department_id', 'username'], + ]); + + $result = $command->getRules($model, [], $args); + $expected = [ + [ + 'name' => 'isUnique', + 'fields' => ['login'], + 'options' => [], + ], + [ + 'name' => 'isUnique', + 'fields' => ['email'], + 'options' => [], + ], + [ + 'name' => 'isUnique', + 'fields' => ['department_id', 'username'], + 'options' => [], + ], + ]; + $this->assertEquals($expected, $result); + } + /** * Test that specific behaviors are auto-detected * diff --git a/tests/comparisons/Model/testBakeAssociationDetectionCategoriesProductsTable.php b/tests/comparisons/Model/testBakeAssociationDetectionCategoriesProductsTable.php index d1f7d554..c5fff2bf 100644 --- a/tests/comparisons/Model/testBakeAssociationDetectionCategoriesProductsTable.php +++ b/tests/comparisons/Model/testBakeAssociationDetectionCategoriesProductsTable.php @@ -63,8 +63,8 @@ public function initialize(array $config): void */ public function buildRules(RulesChecker $rules): RulesChecker { - $rules->add($rules->existsIn('category_id', 'Categories'), ['errorField' => 'category_id']); - $rules->add($rules->existsIn('product_id', 'Products'), ['errorField' => 'product_id']); + $rules->add($rules->existsIn(['category_id'], 'Categories'), ['errorField' => 'category_id']); + $rules->add($rules->existsIn(['product_id'], 'Products'), ['errorField' => 'product_id']); return $rules; } diff --git a/tests/comparisons/Model/testBakeAssociationDetectionProductVersionsTable.php b/tests/comparisons/Model/testBakeAssociationDetectionProductVersionsTable.php index c4c27056..c6723816 100644 --- a/tests/comparisons/Model/testBakeAssociationDetectionProductVersionsTable.php +++ b/tests/comparisons/Model/testBakeAssociationDetectionProductVersionsTable.php @@ -78,7 +78,7 @@ public function validationDefault(Validator $validator): Validator */ public function buildRules(RulesChecker $rules): RulesChecker { - $rules->add($rules->existsIn('product_id', 'Products'), ['errorField' => 'product_id']); + $rules->add($rules->existsIn(['product_id'], 'Products'), ['errorField' => 'product_id']); return $rules; } diff --git a/tests/comparisons/Model/testBakeAssociationDetectionProductVersionsTableSigned.php b/tests/comparisons/Model/testBakeAssociationDetectionProductVersionsTableSigned.php index 3e07ce94..9e3a4a62 100644 --- a/tests/comparisons/Model/testBakeAssociationDetectionProductVersionsTableSigned.php +++ b/tests/comparisons/Model/testBakeAssociationDetectionProductVersionsTableSigned.php @@ -78,7 +78,7 @@ public function validationDefault(Validator $validator): Validator */ public function buildRules(RulesChecker $rules): RulesChecker { - $rules->add($rules->existsIn('product_id', 'Products'), ['errorField' => 'product_id']); + $rules->add($rules->existsIn(['product_id'], 'Products'), ['errorField' => 'product_id']); return $rules; } diff --git a/tests/comparisons/Model/testBakeTableConfig.php b/tests/comparisons/Model/testBakeTableConfig.php index a27066c3..16309d95 100644 --- a/tests/comparisons/Model/testBakeTableConfig.php +++ b/tests/comparisons/Model/testBakeTableConfig.php @@ -113,7 +113,7 @@ public function validationDefault(Validator $validator): Validator */ public function buildRules(RulesChecker $rules): RulesChecker { - $rules->add($rules->existsIn('user_id', 'Users'), ['errorField' => 'user_id']); + $rules->add($rules->existsIn(['user_id'], 'Users'), ['errorField' => 'user_id']); return $rules; } diff --git a/tests/comparisons/Model/testBakeTableWithCounterCache.php b/tests/comparisons/Model/testBakeTableWithCounterCache.php index 5ed0a531..7e5369e0 100644 --- a/tests/comparisons/Model/testBakeTableWithCounterCache.php +++ b/tests/comparisons/Model/testBakeTableWithCounterCache.php @@ -66,7 +66,7 @@ public function initialize(array $config): void */ public function buildRules(RulesChecker $rules): RulesChecker { - $rules->add($rules->existsIn('todo_item_id', 'TodoItems'), ['errorField' => 'todo_item_id']); + $rules->add($rules->existsIn(['todo_item_id'], 'TodoItems'), ['errorField' => 'todo_item_id']); return $rules; } diff --git a/tests/comparisons/Model/testBakeUpdateTableNoFile.php b/tests/comparisons/Model/testBakeUpdateTableNoFile.php index 5b037c52..0d262638 100644 --- a/tests/comparisons/Model/testBakeUpdateTableNoFile.php +++ b/tests/comparisons/Model/testBakeUpdateTableNoFile.php @@ -113,7 +113,7 @@ public function validationDefault(Validator $validator): Validator */ public function buildRules(RulesChecker $rules): RulesChecker { - $rules->add($rules->existsIn('user_id', 'Users'), ['errorField' => 'user_id']); + $rules->add($rules->existsIn(['user_id'], 'Users'), ['errorField' => 'user_id']); return $rules; } From 8fd6290666dae92db0ecf1ea829e605675ac85db Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Wed, 27 Dec 2023 18:47:24 +0100 Subject: [PATCH 2/5] Allow baking enums. (#968) * Allow baking enums. * Allow baking enums. * Rephrase options help. * Update templates/bake/Model/enum.twig Co-authored-by: ADmad * Fix tests * Rename option `backed` to `backing-type` defaulting to `string`. * Clarify backed. * Make backing type a bool, since it defaults anyway on 1 of 2 options. (#971) * Make backing type a bool, since it defaults anyway on 1 of 2 options. * Use --int / -i as flag. --------- Co-authored-by: ADmad Co-authored-by: ADmad --- docs/en/usage.rst | 2 + src/Command/EnumCommand.php | 93 +++++++++++++++++++ templates/bake/Model/enum.twig | 34 +++++++ tests/TestCase/Command/EnumCommandTest.php | 71 ++++++++++++++ tests/comparisons/Model/testBakeEnum.php | 21 +++++ .../Model/testBakeEnumBackedInt.php | 21 +++++ 6 files changed, 242 insertions(+) create mode 100644 src/Command/EnumCommand.php create mode 100644 templates/bake/Model/enum.twig create mode 100644 tests/TestCase/Command/EnumCommandTest.php create mode 100644 tests/comparisons/Model/testBakeEnum.php create mode 100644 tests/comparisons/Model/testBakeEnumBackedInt.php diff --git a/docs/en/usage.rst b/docs/en/usage.rst index e25b3f02..29da6dcb 100644 --- a/docs/en/usage.rst +++ b/docs/en/usage.rst @@ -31,9 +31,11 @@ You can get the list of available bake command by running ``bin/cake bake --help - bake behavior - bake cell - bake command + - bake command_helper - bake component - bake controller - bake controller all + - bake enum - bake fixture - bake fixture all - bake form diff --git a/src/Command/EnumCommand.php b/src/Command/EnumCommand.php new file mode 100644 index 00000000..2118450e --- /dev/null +++ b/src/Command/EnumCommand.php @@ -0,0 +1,93 @@ + + */ + public function templateData(Arguments $arguments): array + { + $data = parent::templateData($arguments); + $data['backingType'] = $arguments->getOption('int') ? 'int' : 'string'; + + return $data; + } + + /** + * Gets the option parser instance and configures it. + * + * @param \Cake\Console\ConsoleOptionParser $parser The option parser to update. + * @return \Cake\Console\ConsoleOptionParser + */ + public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + { + $parser = $this->_setCommonOptions($parser); + + $parser->setDescription( + 'Bake backed enums for use in models.' + )->addOption('int', [ + 'help' => 'Using backed enums with int instead of string as return type', + 'boolean' => true, + 'short' => 'i', + ]); + + return $parser; + } +} diff --git a/templates/bake/Model/enum.twig b/templates/bake/Model/enum.twig new file mode 100644 index 00000000..fe229b11 --- /dev/null +++ b/templates/bake/Model/enum.twig @@ -0,0 +1,34 @@ +{# +/** + * CakePHP(tm) : Rapid Development Framework (https://cakephp.org) + * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) + * + * Licensed under The MIT License + * For full copyright and license information, please see the LICENSE.txt + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) + * @link https://cakephp.org CakePHP(tm) Project + * @since 3.1.0 + * @license https://www.opensource.org/licenses/mit-license.php MIT License + */ +#} +{{ element('Bake.file_header', { + namespace: "#{namespace}\\Model\\Enum", + classImports: [ + 'Cake\\Database\\Type\\EnumLabelInterface', + 'Cake\\Utility\\Inflector', + ], +}) }} + +{{ DocBlock.classDescription(name, 'Enum', [])|raw }} +enum {{ name }}: {{ backingType }} implements EnumLabelInterface +{ + /** + * @return string + */ + public function label(): string + { + return Inflector::humanize(Inflector::underscore($this->name)); + } +} diff --git a/tests/TestCase/Command/EnumCommandTest.php b/tests/TestCase/Command/EnumCommandTest.php new file mode 100644 index 00000000..678aaf40 --- /dev/null +++ b/tests/TestCase/Command/EnumCommandTest.php @@ -0,0 +1,71 @@ +_compareBasePath = Plugin::path('Bake') . 'tests' . DS . 'comparisons' . DS . 'Model' . DS; + $this->setAppNamespace('Bake\Test\App'); + } + + /** + * test baking an enum + * + * @return void + */ + public function testBakeEnum() + { + $this->generatedFile = APP . 'Model/Enum/FooBar.php'; + $this->exec('bake enum FooBar', ['y']); + + $this->assertExitCode(CommandInterface::CODE_SUCCESS); + $this->assertFileExists($this->generatedFile); + $result = file_get_contents($this->generatedFile); + $this->assertSameAsFile(__FUNCTION__ . '.php', $result); + } + + /** + * test baking an enum with int return type + * + * @return void + */ + public function testBakeEnumBackedInt() + { + $this->generatedFile = APP . 'Model/Enum/FooBar.php'; + $this->exec('bake enum FooBar -i', ['y']); + + $this->assertExitCode(CommandInterface::CODE_SUCCESS); + $this->assertFileExists($this->generatedFile); + $result = file_get_contents($this->generatedFile); + $this->assertSameAsFile(__FUNCTION__ . '.php', $result); + } +} diff --git a/tests/comparisons/Model/testBakeEnum.php b/tests/comparisons/Model/testBakeEnum.php new file mode 100644 index 00000000..41c6faea --- /dev/null +++ b/tests/comparisons/Model/testBakeEnum.php @@ -0,0 +1,21 @@ +name)); + } +} diff --git a/tests/comparisons/Model/testBakeEnumBackedInt.php b/tests/comparisons/Model/testBakeEnumBackedInt.php new file mode 100644 index 00000000..c0c0648c --- /dev/null +++ b/tests/comparisons/Model/testBakeEnumBackedInt.php @@ -0,0 +1,21 @@ +name)); + } +} From e7e3c437a510e05ec4c137e522180bae92fed343 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Thu, 28 Dec 2023 09:42:41 +0100 Subject: [PATCH 3/5] Bake table with enums mapped. (#969) --- composer.json | 2 +- src/Command/ModelCommand.php | 45 +++++++ templates/bake/Model/table.twig | 11 ++ tests/TestCase/Command/ModelCommandTest.php | 19 ++- .../Model/testBakeTableWithEnum.php | 116 ++++++++++++++++++ tests/schema.php | 3 +- .../App/Model/Enum/BakeUserStatus.php | 32 +++++ 7 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 tests/comparisons/Model/testBakeTableWithEnum.php create mode 100644 tests/test_app/App/Model/Enum/BakeUserStatus.php diff --git a/composer.json b/composer.json index 426d9aff..afd60c9d 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "require": { "php": ">=8.1", "brick/varexporter": "^0.4.0", - "cakephp/cakephp": "^5.0.0", + "cakephp/cakephp": "^5.0.3", "cakephp/twig-view": "^2.0.0", "nikic/php-parser": "^4.13.2" }, diff --git a/src/Command/ModelCommand.php b/src/Command/ModelCommand.php index aa49069d..7a91e324 100644 --- a/src/Command/ModelCommand.php +++ b/src/Command/ModelCommand.php @@ -1195,6 +1195,7 @@ public function bakeTable(Table $model, array $data, Arguments $args, ConsoleIo 'validation' => [], 'rulesChecker' => [], 'behaviors' => [], + 'enums' => $this->enums($model, $entity, $namespace), 'connection' => $this->connection, 'fileBuilder' => new FileBuilder($io, "{$namespace}\Model\Table", $parsedFile), ]; @@ -1382,4 +1383,48 @@ public function bakeTest(string $className, Arguments $args, ConsoleIo $io): voi ); $test->execute($testArgs, $io); } + + /** + * @param \Cake\ORM\Table $table + * @param string $entity + * @param string $namespace + * @return array + */ + protected function enums(Table $table, string $entity, string $namespace): array + { + $fields = $this->possibleEnumFields($table->getSchema()); + $enumClassNamespace = $namespace . '\Model\Enum\\'; + + $enums = []; + foreach ($fields as $field) { + $enumClassName = $enumClassNamespace . $entity . Inflector::camelize($field); + if (!class_exists($enumClassName)) { + continue; + } + + $enums[$field] = $enumClassName; + } + + return $enums; + } + + /** + * @param \Cake\Database\Schema\TableSchemaInterface $schema + * @return array + */ + protected function possibleEnumFields(TableSchemaInterface $schema): array + { + $fields = []; + + foreach ($schema->columns() as $column) { + $columnSchema = $schema->getColumn($column); + if (!in_array($columnSchema['type'], ['string', 'integer', 'tinyinteger', 'smallinteger'], true)) { + continue; + } + + $fields[] = $column; + } + + return $fields; + } } diff --git a/templates/bake/Model/table.twig b/templates/bake/Model/table.twig index 5a1b9620..0b298813 100644 --- a/templates/bake/Model/table.twig +++ b/templates/bake/Model/table.twig @@ -61,6 +61,17 @@ class {{ name }}Table extends Table{{ fileBuilder.classBuilder.implements ? ' im {%- endif %} {% endif %} +{%- if enums %} + +{% endif %} + +{%- if enums %} + +{%- for name, className in enums %} + $this->getSchema()->setColumnType('{{ name }}', \Cake\Database\Type\EnumType::from(\{{ className }}::class)); +{% endfor %} +{% endif %} + {%- if behaviors %} {% endif %} diff --git a/tests/TestCase/Command/ModelCommandTest.php b/tests/TestCase/Command/ModelCommandTest.php index 47694cb1..ad53673c 100644 --- a/tests/TestCase/Command/ModelCommandTest.php +++ b/tests/TestCase/Command/ModelCommandTest.php @@ -1973,7 +1973,24 @@ public function testBakeTableWithPlugin() } /** - * test generation with counter cach + * test generation with enum + * + * @return void + */ + public function testBakeTableWithEnum(): void + { + $this->generatedFile = APP . 'Model/Table/BakeUsersTable.php'; + + $this->exec('bake model --no-validation --no-test --no-fixture --no-entity BakeUsers'); + + $this->assertExitCode(CommandInterface::CODE_SUCCESS); + $this->assertFileExists($this->generatedFile); + $result = file_get_contents($this->generatedFile); + $this->assertStringContainsString('$this->getSchema()->setColumnType(\'status\', \Cake\Database\Type\EnumType::from(\Bake\Test\App\Model\Enum\BakeUserStatus::class));', $result); + } + + /** + * test generation with counter cache * * @return void */ diff --git a/tests/comparisons/Model/testBakeTableWithEnum.php b/tests/comparisons/Model/testBakeTableWithEnum.php new file mode 100644 index 00000000..07a54bbb --- /dev/null +++ b/tests/comparisons/Model/testBakeTableWithEnum.php @@ -0,0 +1,116 @@ + newEntities(array $data, array $options = []) + * @method \Bake\Test\App\Model\Entity\TestBakeArticle get(mixed $primaryKey, array|string $finder = 'all', \Psr\SimpleCache\CacheInterface|string|null $cache = null, \Closure|string|null $cacheKey = null, mixed ...$args) + * @method \Bake\Test\App\Model\Entity\TestBakeArticle findOrCreate($search, ?callable $callback = null, array $options = []) + * @method \Bake\Test\App\Model\Entity\TestBakeArticle patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = []) + * @method array<\Bake\Test\App\Model\Entity\TestBakeArticle> patchEntities(iterable $entities, array $data, array $options = []) + * @method \Bake\Test\App\Model\Entity\TestBakeArticle|false save(\Cake\Datasource\EntityInterface $entity, array $options = []) + * @method \Bake\Test\App\Model\Entity\TestBakeArticle saveOrFail(\Cake\Datasource\EntityInterface $entity, array $options = []) + * @method iterable<\Bake\Test\App\Model\Entity\TestBakeArticle>|\Cake\Datasource\ResultSetInterface<\Bake\Test\App\Model\Entity\TestBakeArticle>|false saveMany(iterable $entities, array $options = []) + * @method iterable<\Bake\Test\App\Model\Entity\TestBakeArticle>|\Cake\Datasource\ResultSetInterface<\Bake\Test\App\Model\Entity\TestBakeArticle> saveManyOrFail(iterable $entities, array $options = []) + * @method iterable<\Bake\Test\App\Model\Entity\TestBakeArticle>|\Cake\Datasource\ResultSetInterface<\Bake\Test\App\Model\Entity\TestBakeArticle>|false deleteMany(iterable $entities, array $options = []) + * @method iterable<\Bake\Test\App\Model\Entity\TestBakeArticle>|\Cake\Datasource\ResultSetInterface<\Bake\Test\App\Model\Entity\TestBakeArticle> deleteManyOrFail(iterable $entities, array $options = []) + * + * @mixin \Cake\ORM\Behavior\TimestampBehavior + */ +class TestBakeArticlesTable extends Table +{ + /** + * Initialize method + * + * @param array $config The configuration for the Table. + * @return void + */ + public function initialize(array $config): void + { + parent::initialize($config); + + $this->setTable('bake_articles'); + $this->setDisplayField('title'); + $this->setPrimaryKey('id'); + + $this->addBehavior('Timestamp'); + + $this->getSchema()->setColumnType('status', EnumType::from(BakeUserStatus::class)); + + $this->belongsTo('BakeUsers', [ + 'foreignKey' => 'bake_user_id', + 'joinType' => 'INNER', + ]); + $this->hasMany('BakeComments', [ + 'foreignKey' => 'bake_article_id', + ]); + $this->belongsToMany('BakeTags', [ + 'foreignKey' => 'bake_article_id', + 'targetForeignKey' => 'bake_tag_id', + 'joinTable' => 'bake_articles_bake_tags', + ]); + } + + /** + * Default validation rules. + * + * @param \Cake\Validation\Validator $validator Validator instance. + * @return \Cake\Validation\Validator + */ + public function validationDefault(Validator $validator): Validator + { + $validator + ->numeric('id') + ->allowEmptyString('id', 'create'); + + $validator + ->scalar('name') + ->maxLength('name', 100, 'Name must be shorter than 100 characters.') + ->requirePresence('name', 'create') + ->allowEmptyString('name', null, false); + + $validator + ->nonNegativeInteger('count') + ->requirePresence('count', 'create') + ->allowEmptyString('count', null, false); + + $validator + ->greaterThanOrEqual('price', 0) + ->requirePresence('price', 'create') + ->allowEmptyString('price', null, false); + + $validator + ->email('email') + ->add('email', 'unique', ['rule' => 'validateUnique', 'provider' => 'table']) + ->allowEmptyString('email'); + + $validator + ->uploadedFile('image', [ + 'optional' => true, + 'types' => ['image/jpeg'], + ]) + ->allowEmptyFile('image'); + + return $validator; + } + + /** + * Returns the database connection name to use by default. + * + * @return string + */ + public static function defaultConnectionName(): string + { + return 'test'; + } +} diff --git a/tests/schema.php b/tests/schema.php index 4075cad5..561ff0f7 100644 --- a/tests/schema.php +++ b/tests/schema.php @@ -208,7 +208,7 @@ 'body' => 'text', 'rating' => ['type' => 'float', 'unsigned' => true, 'default' => 0.0, 'null' => false], 'score' => ['type' => 'decimal', 'unsigned' => true, 'default' => 0.0, 'null' => false], - 'published' => ['type' => 'boolean', 'length' => 1, 'default' => false, 'null' => false], + 'published' => ['type' => 'boolean', 'length' => 1, 'default' => false], 'created' => 'datetime', 'updated' => 'datetime', ], @@ -377,6 +377,7 @@ 'id' => ['type' => 'integer'], 'username' => ['type' => 'string', 'null' => true, 'length' => 255], 'password' => ['type' => 'string', 'null' => true, 'length' => 255], + 'status' => ['type' => 'tinyinteger', 'length' => 2, 'default' => null, 'null' => true], 'created' => ['type' => 'timestamp', 'null' => true], 'updated' => ['type' => 'timestamp', 'null' => true], ], diff --git a/tests/test_app/App/Model/Enum/BakeUserStatus.php b/tests/test_app/App/Model/Enum/BakeUserStatus.php new file mode 100644 index 00000000..be156019 --- /dev/null +++ b/tests/test_app/App/Model/Enum/BakeUserStatus.php @@ -0,0 +1,32 @@ +name)); + } +} From c7cb269834933173596899f20e4f6e429ed7d436 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Tue, 2 Jan 2024 14:49:16 +0100 Subject: [PATCH 4/5] Complete baking of enums. (#972) * Complete baking of enums. * Update src/Command/EnumCommand.php Co-authored-by: othercorey * Fix up index/view for enums. * Fix up index/view for enums. * Switch key/value, use CamelCase * Stricter validation * Update src/Command/ModelCommand.php Co-authored-by: Mark Story * Cleanup * Cleanup * Add more tests. --------- Co-authored-by: othercorey Co-authored-by: Mark Story --- src/Command/EnumCommand.php | 86 +++++++++++++- src/Command/ModelCommand.php | 108 +++++++++++++++++- src/Utility/Model/EnumParser.php | 45 ++++++++ src/View/Helper/BakeHelper.php | 4 + templates/bake/Model/enum.twig | 4 + templates/bake/Template/index.twig | 4 +- templates/bake/Template/view.twig | 13 +++ tests/TestCase/Command/EnumCommandTest.php | 32 ++++++ tests/TestCase/Command/ModelCommandTest.php | 29 +++++ .../TestCase/Utility/Model/EnumParserTest.php | 47 ++++++++ .../Model/testBakeEnumBackedIntWithCases.php | 25 ++++ .../Model/testBakeEnumBackedWithCases.php | 25 ++++ .../Model/testBakeTableWithEnumConfig.php | 25 ++++ .../App/Model/Enum/BakeUserNullableGender.php | 25 ++++ .../App/Model/Enum/BakeUserStatus.php | 6 +- 15 files changed, 468 insertions(+), 10 deletions(-) create mode 100644 src/Utility/Model/EnumParser.php create mode 100644 tests/TestCase/Utility/Model/EnumParserTest.php create mode 100644 tests/comparisons/Model/testBakeEnumBackedIntWithCases.php create mode 100644 tests/comparisons/Model/testBakeEnumBackedWithCases.php create mode 100644 tests/comparisons/Model/testBakeTableWithEnumConfig.php create mode 100644 tests/test_app/App/Model/Enum/BakeUserNullableGender.php diff --git a/src/Command/EnumCommand.php b/src/Command/EnumCommand.php index 2118450e..c5ff42bc 100644 --- a/src/Command/EnumCommand.php +++ b/src/Command/EnumCommand.php @@ -16,8 +16,12 @@ */ namespace Bake\Command; +use Bake\Utility\Model\EnumParser; use Cake\Console\Arguments; +use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; +use Cake\Utility\Inflector; +use InvalidArgumentException; /** * Enum code generator. @@ -64,8 +68,20 @@ public function template(): string */ public function templateData(Arguments $arguments): array { + $cases = EnumParser::parseCases($arguments->getArgument('cases'), (bool)$arguments->getOption('int')); + $isOfTypeInt = $this->isOfTypeInt($cases); + $backingType = $isOfTypeInt ? 'int' : 'string'; + if ($arguments->getOption('int')) { + if ($cases && !$isOfTypeInt) { + throw new InvalidArgumentException('Cases do not match requested `int` backing type.'); + } + + $backingType = 'int'; + } + $data = parent::templateData($arguments); - $data['backingType'] = $arguments->getOption('int') ? 'int' : 'string'; + $data['backingType'] = $backingType; + $data['cases'] = $this->formatCases($cases); return $data; } @@ -82,12 +98,76 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar $parser->setDescription( 'Bake backed enums for use in models.' - )->addOption('int', [ - 'help' => 'Using backed enums with int instead of string as return type', + )->addArgument('name', [ + 'help' => 'Name of the enum to bake. You can use Plugin.name to bake plugin enums.', + 'required' => true, + ])->addArgument('cases', [ + 'help' => 'List of either `one,two` for string or `foo:0,bar:1` for int type.', + ])->addOption('int', [ + 'help' => 'Using backed enums with int instead of string as return type.', 'boolean' => true, 'short' => 'i', ]); return $parser; } + + /** + * @param array $definition + * @return bool + */ + protected function isOfTypeInt(array $definition): bool + { + if (!$definition) { + return false; + } + + foreach ($definition as $value) { + if (!is_int($value)) { + return false; + } + } + + return true; + } + + /** + * @param array $cases + * @return array + */ + protected function formatCases(array $cases): array + { + $formatted = []; + foreach ($cases as $case => $value) { + $case = Inflector::camelize(Inflector::underscore($case)); + if (is_string($value)) { + $value = '\'' . $value . '\''; + } + $formatted[] = 'case ' . $case . ' = ' . $value . ';'; + } + + return $formatted; + } + + /** + * Generate a class stub + * + * @param string $name The class name + * @param \Cake\Console\Arguments $args The console arguments + * @param \Cake\Console\ConsoleIo $io The console io + * @return void + */ + protected function bake(string $name, Arguments $args, ConsoleIo $io): void + { + parent::bake($name, $args, $io); + + $path = $this->getPath($args); + $filename = $path . $name . '.php'; + + // Work around composer caching that classes/files do not exist. + // Check for the file as it might not exist in tests. + if (file_exists($filename)) { + require_once $filename; + } + } } diff --git a/src/Command/ModelCommand.php b/src/Command/ModelCommand.php index c8fd2bae..afcf486d 100644 --- a/src/Command/ModelCommand.php +++ b/src/Command/ModelCommand.php @@ -17,6 +17,7 @@ namespace Bake\Command; use Bake\CodeGen\FileBuilder; +use Bake\Utility\Model\EnumParser; use Bake\Utility\TableScanner; use Cake\Console\Arguments; use Cake\Console\ConsoleIo; @@ -28,9 +29,12 @@ use Cake\Database\Schema\CachedCollection; use Cake\Database\Schema\TableSchema; use Cake\Database\Schema\TableSchemaInterface; +use Cake\Database\Type\EnumType; +use Cake\Database\TypeFactory; use Cake\Datasource\ConnectionManager; use Cake\ORM\Table; use Cake\Utility\Inflector; +use ReflectionEnum; use function Cake\Core\pluginSplit; /** @@ -111,6 +115,8 @@ public function bake(string $name, Arguments $args, ConsoleIo $io): void $tableObject = $this->getTableObject($name, $table); $this->validateNames($tableObject->getSchema(), $io); $data = $this->getTableContext($tableObject, $table, $name, $args, $io); + + $this->bakeEnums($tableObject, $data, $args, $io); $this->bakeTable($tableObject, $data, $args, $io); $this->bakeEntity($tableObject, $data, $args, $io); $this->bakeFixture($tableObject->getAlias(), $tableObject->getTable(), $args, $io); @@ -168,6 +174,7 @@ public function getTableContext( $behaviors = $this->getBehaviors($tableObject); $connection = $this->connection; $hidden = $this->getHiddenFields($tableObject, $args); + $enumSchema = $this->getEnumDefinitions($tableObject->getSchema()); return compact( 'associations', @@ -181,7 +188,8 @@ public function getTableContext( 'rulesChecker', 'behaviors', 'connection', - 'hidden' + 'hidden', + 'enumSchema', ); } @@ -1118,7 +1126,7 @@ public function getCounterCache(Table $model): array * Bake an entity class. * * @param \Cake\ORM\Table $model Model name or object - * @param array $data An array to use to generate the Table + * @param array $data An array to use to generate the Table * @param \Cake\Console\Arguments $args CLI Arguments * @param \Cake\Console\ConsoleIo $io CLI io * @return void @@ -1170,7 +1178,7 @@ public function bakeEntity(Table $model, array $data, Arguments $args, ConsoleIo * Bake a table class. * * @param \Cake\ORM\Table $model Model name or object - * @param array $data An array to use to generate the Table + * @param array $data An array to use to generate the Table * @param \Cake\Console\Arguments $args CLI Arguments * @param \Cake\Console\ConsoleIo $io CLI Arguments * @return void @@ -1435,6 +1443,12 @@ protected function possibleEnumFields(TableSchemaInterface $schema): array foreach ($schema->columns() as $column) { $columnSchema = $schema->getColumn($column); + if (str_starts_with($columnSchema['type'], 'enum-')) { + $fields[] = $column; + + continue; + } + if (!in_array($columnSchema['type'], ['string', 'integer', 'tinyinteger', 'smallinteger'], true)) { continue; } @@ -1444,4 +1458,92 @@ protected function possibleEnumFields(TableSchemaInterface $schema): array return $fields; } + + /** + * @param \Cake\Database\Schema\TableSchemaInterface $schema + * @return array + */ + protected function getEnumDefinitions(TableSchemaInterface $schema): array + { + $enums = []; + + foreach ($schema->columns() as $column) { + $columnSchema = $schema->getColumn($column); + if ( + !in_array($columnSchema['type'], ['string', 'integer', 'tinyinteger', 'smallinteger'], true) + && !str_starts_with($columnSchema['type'], 'enum-') + ) { + continue; + } + + if (empty($columnSchema['comment']) || strpos($columnSchema['comment'], '[enum]') === false) { + continue; + } + + $enumsDefinitionString = trim(mb_substr($columnSchema['comment'], strpos($columnSchema['comment'], '[enum]') + 6)); + $isInt = in_array($columnSchema['type'], ['integer', 'tinyinteger', 'smallinteger'], true); + if (str_starts_with($columnSchema['type'], 'enum-')) { + $dbType = TypeFactory::build($columnSchema['type']); + if ($dbType instanceof EnumType) { + $class = $dbType->getEnumClassName(); + $reflectionEnum = new ReflectionEnum($class); + $backingType = (string)$reflectionEnum->getBackingType(); + if ($backingType === 'int') { + $isInt = true; + } + } + } + $enumsDefinition = EnumParser::parseCases($enumsDefinitionString, $isInt); + if (!$enumsDefinition) { + continue; + } + + $enums[$column] = [ + 'type' => $isInt ? 'int' : 'string', + 'cases' => $enumsDefinition, + ]; + } + + return $enums; + } + + /** + * @param \Cake\ORM\Table $model + * @param array $data + * @param \Cake\Console\Arguments $args + * @param \Cake\Console\ConsoleIo $io + * @return void + */ + protected function bakeEnums(Table $model, array $data, Arguments $args, ConsoleIo $io): void + { + $enums = $data['enumSchema']; + if (!$enums) { + return; + } + + $entity = $this->_entityName($model->getAlias()); + + foreach ($enums as $column => $data) { + $enumCommand = new EnumCommand(); + + $name = $entity . Inflector::camelize($column); + if ($this->plugin) { + $name = $this->plugin . '.' . $name; + } + + $enumCases = $data['cases']; + + $cases = []; + foreach ($enumCases as $k => $v) { + $cases[] = $k . ':' . $v; + } + + $args = new Arguments( + [$name, implode(',', $cases)], + ['int' => $data['type'] === 'int'] + $args->getOptions(), + ['name', 'cases'] + ); + $enumCommand->execute($args, $io); + } + } } diff --git a/src/Utility/Model/EnumParser.php b/src/Utility/Model/EnumParser.php new file mode 100644 index 00000000..df3ed5dc --- /dev/null +++ b/src/Utility/Model/EnumParser.php @@ -0,0 +1,45 @@ + + */ + public static function parseCases(?string $casesString, bool $int): array + { + if ($casesString === null || $casesString === '') { + return []; + } + + $enumCases = explode(',', $casesString); + + $definition = []; + foreach ($enumCases as $k => $enumCase) { + $case = $value = trim($enumCase); + if (str_contains($case, ':')) { + $value = trim(mb_substr($case, strpos($case, ':') + 1)); + $case = mb_substr($case, 0, strpos($case, ':')); + } elseif ($int) { + $value = $k; + } + + if (!preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $case)) { + throw new InvalidArgumentException(sprintf('`%s` is not a valid enum case', $case)); + } + if (is_string($value) && str_contains($value, '\'')) { + throw new InvalidArgumentException(sprintf('`%s` value cannot contain `\'` character', $case)); + } + + $definition[$case] = $int ? (int)$value : $value; + } + + return $definition; + } +} diff --git a/src/View/Helper/BakeHelper.php b/src/View/Helper/BakeHelper.php index 95572064..52e11dd6 100644 --- a/src/View/Helper/BakeHelper.php +++ b/src/View/Helper/BakeHelper.php @@ -233,6 +233,9 @@ public function getViewFieldsData(array $fields, SchemaInterface $schema, array if (isset($associationFields[$field])) { return 'string'; } + if ($type && str_starts_with($type, 'enum-')) { + return 'enum'; + } $numberTypes = ['decimal', 'biginteger', 'integer', 'float', 'smallinteger', 'tinyinteger']; if (in_array($type, $numberTypes, true)) { return 'number'; @@ -258,6 +261,7 @@ public function getViewFieldsData(array $fields, SchemaInterface $schema, array 'number' => [], 'string' => [], 'boolean' => [], + 'enum' => [], 'date' => [], 'text' => [], ]; diff --git a/templates/bake/Model/enum.twig b/templates/bake/Model/enum.twig index fe229b11..83f48243 100644 --- a/templates/bake/Model/enum.twig +++ b/templates/bake/Model/enum.twig @@ -24,6 +24,10 @@ {{ DocBlock.classDescription(name, 'Enum', [])|raw }} enum {{ name }}: {{ backingType }} implements EnumLabelInterface { +{% if cases %} + {{ Bake.concat('\n ', cases) }} + +{% endif %} /** * @return string */ diff --git a/templates/bake/Template/index.twig b/templates/bake/Template/index.twig index 8651c8f2..0c630067 100644 --- a/templates/bake/Template/index.twig +++ b/templates/bake/Template/index.twig @@ -49,7 +49,9 @@ {% endif %} {% if isKey is not same as(true) %} {% set columnData = Bake.columnData(field, schema) %} -{% if columnData.type not in ['integer', 'float', 'decimal', 'biginteger', 'smallinteger', 'tinyinteger'] %} +{% if columnData.type starts with 'enum-' %} + {{ field }} === null ? '' : h(${{ singularVar }}->{{ field }}->label()) ?> +{% elseif columnData.type not in ['integer', 'float', 'decimal', 'biginteger', 'smallinteger', 'tinyinteger'] %} {{ field }}) ?> {% elseif columnData.null %} {{ field }} === null ? '' : $this->Number->format(${{ singularVar }}->{{ field }}) ?> diff --git a/templates/bake/Template/view.twig b/templates/bake/Template/view.twig index 6f8f2552..ae63d0e2 100644 --- a/templates/bake/Template/view.twig +++ b/templates/bake/Template/view.twig @@ -76,6 +76,19 @@ {% endfor %} {% endif %} +{% if groupedFields.enum %} +{% for field in groupedFields.enum %} + + +{% set columnData = Bake.columnData(field, schema) %} +{% if columnData.null %} + {{ field }} === null ? '' : h(${{ singularVar }}->{{ field }}->label()) ?> +{% else %} + {{ field }}->label()) ?> +{% endif %} + +{% endfor %} +{% endif %} {% if groupedFields.date %} {% for field in groupedFields.date %} diff --git a/tests/TestCase/Command/EnumCommandTest.php b/tests/TestCase/Command/EnumCommandTest.php index 678aaf40..bc34dcb1 100644 --- a/tests/TestCase/Command/EnumCommandTest.php +++ b/tests/TestCase/Command/EnumCommandTest.php @@ -68,4 +68,36 @@ public function testBakeEnumBackedInt() $result = file_get_contents($this->generatedFile); $this->assertSameAsFile(__FUNCTION__ . '.php', $result); } + + /** + * test baking an enum with string return type and cases + * + * @return void + */ + public function testBakeEnumBackedWithCases() + { + $this->generatedFile = APP . 'Model/Enum/FooBar.php'; + $this->exec('bake enum FooBar foo,bar:b,bar_baz', ['y']); + + $this->assertExitCode(CommandInterface::CODE_SUCCESS); + $this->assertFileExists($this->generatedFile); + $result = file_get_contents($this->generatedFile); + $this->assertSameAsFile(__FUNCTION__ . '.php', $result); + } + + /** + * test baking an enum with string return type and cases + * + * @return void + */ + public function testBakeEnumBackedIntWithCases() + { + $this->generatedFile = APP . 'Model/Enum/FooBar.php'; + $this->exec('bake enum FooBar foo,bar,bar_baz:9 -i', ['y']); + + $this->assertExitCode(CommandInterface::CODE_SUCCESS); + $this->assertFileExists($this->generatedFile); + $result = file_get_contents($this->generatedFile); + $this->assertSameAsFile(__FUNCTION__ . '.php', $result); + } } diff --git a/tests/TestCase/Command/ModelCommandTest.php b/tests/TestCase/Command/ModelCommandTest.php index 49ec8f45..731cbd2a 100644 --- a/tests/TestCase/Command/ModelCommandTest.php +++ b/tests/TestCase/Command/ModelCommandTest.php @@ -2111,6 +2111,35 @@ public function testBakeTableWithEnum(): void $this->assertStringContainsString('$this->getSchema()->setColumnType(\'status\', \Cake\Database\Type\EnumType::from(\Bake\Test\App\Model\Enum\BakeUserStatus::class));', $result); } + /** + * test generation with enum config in column comment + * + * @return void + */ + public function testBakeTableWithEnumConfig(): void + { + $this->generatedFile = APP . 'Model/Table/BakeUsersTable.php'; + + $bakeUsers = $this->getTableLocator()->get('BakeUsers'); + $attributes = [ + 'type' => 'string', + 'null' => true, + 'comment' => '[enum]male,female,diverse', + ]; + $bakeUsers->setSchema($bakeUsers->getSchema()->addColumn('nullable_gender', $attributes)); + + $this->exec('bake model --no-validation --no-test --no-fixture --no-entity BakeUsers', ['y']); + + $this->assertExitCode(CommandInterface::CODE_SUCCESS); + $this->assertFileExists($this->generatedFile); + $result = file_get_contents($this->generatedFile); + $this->assertStringContainsString('$this->getSchema()->setColumnType(\'nullable_gender\', \Cake\Database\Type\EnumType::from(\Bake\Test\App\Model\Enum\BakeUserNullableGender::class));', $result); + + $generatedEnumFile = APP . 'Model/Enum/BakeUserNullableGender.php'; + $result = file_get_contents($generatedEnumFile); + $this->assertSameAsFile(__FUNCTION__ . '.php', $result); + } + /** * test generation with counter cache * diff --git a/tests/TestCase/Utility/Model/EnumParserTest.php b/tests/TestCase/Utility/Model/EnumParserTest.php new file mode 100644 index 00000000..bff2c71c --- /dev/null +++ b/tests/TestCase/Utility/Model/EnumParserTest.php @@ -0,0 +1,47 @@ +assertSame([], $cases); + + $cases = EnumParser::parseCases('foo, bar', false); + $this->assertSame(['foo' => 'foo', 'bar' => 'bar'], $cases); + + $cases = EnumParser::parseCases('foo:f, bar:b', false); + $this->assertSame(['foo' => 'f', 'bar' => 'b'], $cases); + + $cases = EnumParser::parseCases('foo:0, bar:1', true); + $this->assertSame(['foo' => 0, 'bar' => 1], $cases); + + $cases = EnumParser::parseCases('foo, bar', true); + $this->assertSame(['foo' => 0, 'bar' => 1], $cases); + } +} diff --git a/tests/comparisons/Model/testBakeEnumBackedIntWithCases.php b/tests/comparisons/Model/testBakeEnumBackedIntWithCases.php new file mode 100644 index 00000000..fcd88e07 --- /dev/null +++ b/tests/comparisons/Model/testBakeEnumBackedIntWithCases.php @@ -0,0 +1,25 @@ +name)); + } +} diff --git a/tests/comparisons/Model/testBakeEnumBackedWithCases.php b/tests/comparisons/Model/testBakeEnumBackedWithCases.php new file mode 100644 index 00000000..403e8fda --- /dev/null +++ b/tests/comparisons/Model/testBakeEnumBackedWithCases.php @@ -0,0 +1,25 @@ +name)); + } +} diff --git a/tests/comparisons/Model/testBakeTableWithEnumConfig.php b/tests/comparisons/Model/testBakeTableWithEnumConfig.php new file mode 100644 index 00000000..1287d1ac --- /dev/null +++ b/tests/comparisons/Model/testBakeTableWithEnumConfig.php @@ -0,0 +1,25 @@ +name)); + } +} diff --git a/tests/test_app/App/Model/Enum/BakeUserNullableGender.php b/tests/test_app/App/Model/Enum/BakeUserNullableGender.php new file mode 100644 index 00000000..1287d1ac --- /dev/null +++ b/tests/test_app/App/Model/Enum/BakeUserNullableGender.php @@ -0,0 +1,25 @@ +name)); + } +} diff --git a/tests/test_app/App/Model/Enum/BakeUserStatus.php b/tests/test_app/App/Model/Enum/BakeUserStatus.php index be156019..5dcf0ec7 100644 --- a/tests/test_app/App/Model/Enum/BakeUserStatus.php +++ b/tests/test_app/App/Model/Enum/BakeUserStatus.php @@ -19,14 +19,14 @@ enum BakeUserStatus: int implements EnumLabelInterface { - case INACTIVE = 0; - case ACTIVE = 1; + case Inactive = 0; + case Active = 1; /** * @return string */ public function label(): string { - return Inflector::humanize(mb_strtolower($this->name)); + return Inflector::humanize(Inflector::underscore($this->name)); } } From 078ca3c57244396cc8259ae70dc134caa02a3755 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 28 Jan 2024 23:38:00 +0100 Subject: [PATCH 5/5] Also test 3.next --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2b92f09..6c7ee147 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: - 2.x - 2.next - 3.x + - 3.next pull_request: branches: - '*'