From 386f5b7c40430fe6986ad33cd368c2a158f45f4a Mon Sep 17 00:00:00 2001 From: ndm2 Date: Tue, 19 Dec 2023 17:25:06 +0100 Subject: [PATCH] Fix fields being incorrectly detected as file fields. Makes detection more strict by requiring the looked up words to be positioned at the end of the field name, and possible leading characters to be a separator. refs #957 --- 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 | 31 +++++ 7 files changed, 224 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..4f4b5934 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'], 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..9a9c4193 --- /dev/null +++ b/tests/test_app/App/Model/Enum/BakeUserStatus.php @@ -0,0 +1,31 @@ +name); + } +}