Skip to content

Commit

Permalink
Fix fields being incorrectly detected as file fields.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ndm2 authored and dereuromark committed Dec 27, 2023
1 parent 206758c commit 386f5b7
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 3 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
45 changes: 45 additions & 0 deletions src/Command/ModelCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
];
Expand Down Expand Up @@ -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<string, class-string>
*/
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<string>
*/
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;
}
}
11 changes: 11 additions & 0 deletions templates/bake/Model/table.twig
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
19 changes: 18 additions & 1 deletion tests/TestCase/Command/ModelCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
116 changes: 116 additions & 0 deletions tests/comparisons/Model/testBakeTableWithEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);

namespace Bake\Test\App\Model\Table;

use Bake\Test\App\Model\Enum\BakeUserStatus;
use Cake\Database\Type\EnumType;
use Cake\ORM\Table;
use Cake\Validation\Validator;

/**
* TestBakeArticles Model
*
* @method \Bake\Test\App\Model\Entity\TestBakeArticle newEmptyEntity()
* @method \Bake\Test\App\Model\Entity\TestBakeArticle newEntity(array $data, array $options = [])
* @method array<\Bake\Test\App\Model\Entity\TestBakeArticle> 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<string, mixed> $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';
}
}
3 changes: 2 additions & 1 deletion tests/schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
Expand Down Expand Up @@ -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],
],
Expand Down
31 changes: 31 additions & 0 deletions tests/test_app/App/Model/Enum/BakeUserStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);

/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @since 3.1.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Bake\Test\App\Model\Enum;

use Cake\Database\Type\EnumLabelInterface;

enum BakeUserStatus: int implements EnumLabelInterface
{
case ACTIVE = 1;
case INACTIVE = 0;

/**
* @return string
*/
public function label(): string
{
return mb_strtolower($this->name);
}
}

0 comments on commit 386f5b7

Please sign in to comment.