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); + } +}