diff --git a/CHANGELOG.md b/CHANGELOG.md index f91107c86..fe8ef329d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ Yii Framework 2 gii extension Change Log 2.2.8 under development ----------------------- -- no changes in this release. - +- Bug #558: Fix an issue with creating rules for multiple fields with the same default values (carono) +- Enh #560: Remove unnecessary files from Composer package (@s1lver) 2.2.7 February 13, 2025 ----------------------- diff --git a/src/generators/model/EnumGenerator.php b/src/generators/model/EnumGenerator.php new file mode 100644 index 000000000..2e8390e1e --- /dev/null +++ b/src/generators/model/EnumGenerator.php @@ -0,0 +1,221 @@ + 'exclamation', + '@' => 'at', + '#' => 'number', + '$' => 'dollar', + '%' => 'percent', + '^' => 'caret', + '&' => 'and', + '*' => 'asterisk', + '(' => 'open_parenthesis', + ')' => 'close_parenthesis', + '=' => 'equals', + '+' => 'plus', + '{' => 'open_curly_brace', + '}' => 'close_curly_brace', + '[' => 'open_square_bracket', + ']' => 'close_square_bracket', + '|' => 'pipe', + '\\' => 'backslash', + '/' => 'forward_slash', + ':' => 'colon', + ';' => 'semicolon', + '"' => 'double_quote', + '\'' => 'single_quote', + '<' => 'less_than', + '>' => 'greater_than', + ',' => 'comma', + '.' => 'dot', + '?' => 'question_mark', + '~' => 'tilde', + '`' => 'backtick' + ]; + + /** + * @var string[] + */ + private static $_symbolsAbbrevationList; + + /** @var ColumnSchema */ + private $column; + + + /** + * @var null|array + */ + private $_constantList; + + private $_generator; + + /** + * @param ColumnSchema[] $columns + * @return EnumGenerator[] + */ + public static function loadEnumColumns($generator, $columns) + { + $enumColumns = []; + foreach ($columns as $column) { + if (empty($column->enumValues)) { + continue; + } + if (stripos($column->dbType, 'ENUM') !== 0) { + continue; + } + $enumColumns[$column->name] = new self($generator, $column); + } + return $enumColumns; + } + + public static function symbolsAbbrevationList() + { + if (self::$_symbolsAbbrevationList) { + return self::$_symbolsAbbrevationList; + } + return self::$_symbolsAbbrevationList = array_map(static function ($item) { + return ' ' . $item . ' '; + }, self::$symbolsAbbrevation); + } + + public function __construct($generator, $column) + { + $this->_generator = $generator; + $this->column = $column; + } + + public function enumConstantList() + { + if ($this->_constantList !== null) { + return $this->_constantList; + } + $list = []; + $constantEnumValues = []; + foreach ($this->column->enumValues as $value) { + $constantName = self::createConstantName($this->column->name, $value); + if (in_array($constantName, $list, true)) { + $this->_generator->addError('tableName', "Enum column '{$this->column->name}' has generated duplicate constant name '{$constantName}' for enum value '{$value}'."); + } + $list[$constantName] = [ + 'constantName' => $constantName, + 'value' => $value, + ]; + $constantEnumValues[$constantName][] = $value; + } + foreach ($constantEnumValues as $enumConstantName => $enumValues) { + if (count($enumValues) === 1) { + continue; + } + $values = implode("', '", $enumValues); + $this + ->_generator + ->addError( + 'tableName', + "Enum column '{$this->getColumnsName()}' has generated duplicate constant names '{$enumConstantName}' for enum values '{$values}'." + ); + } + return $this->_constantList = $list; + } + + private static function createValueForName($value) + { + /** + * Replaces all non-alphabetical symbols with their respective names. + * Exceptions: + * - "_" and " " - considered as separators + * - "-" - treated as a special case and processed in the next statement + */ + $value = strtr($value, self::symbolsAbbrevationList()); + + /** + * Replaces "-" with "minus": + * - if "-" is at the beginning of the string + * - if "space" precedes "-" + * - if "-" is at the end of the string + * In all other cases, it is treated as a separator. + */ + return preg_replace('#^-| -|-$#', ' minus ', $value); + } + + /** + * @return string + */ + public function createRule() + { + return "['" . $this->column->name . "', 'in', 'range' => array_keys(self::" . $this->createOptsFunctionName() . '())]'; + } + + /** + * @return string + */ + public function createOptsFunctionName() + { + return 'opts' . $this->createColumnCamelName(); + } + + /** + * @return string + */ + public function createDisplayFunctionName() + { + return 'display' . $this->createColumnCamelName(); + } + + /** + * @return string + */ + public function createIsFunctionName($value) + { + return 'is' + . $this->createColumnCamelName() + . self::createValueForFunction($value); + } + + /** + * @return string + */ + public function createSetFunctionName($value) + { + return 'set' + . $this->createColumnCamelName() + . 'To' + . self::createValueForFunction($value); + } + + /** + * @param string $value + * @return string + */ + private static function createValueForFunction($value) + { + return Inflector::id2camel(Inflector::slug(self::createValueForName($value))); + } + + private static function createConstantName($columnName, $value) + { + return strtoupper(Inflector::slug($columnName . ' ' . self::createValueForName($value), '_')); + } + + /** + * @return string + */ + private function createColumnCamelName() + { + return Inflector::id2camel(Inflector::id2camel($this->column->name, '_')); + } + + /** + * @return string + */ + public function getColumnsName() + { + return $this->column->name; + } +} diff --git a/src/generators/model/Generator.php b/src/generators/model/Generator.php index 86abe3522..8e07c03bb 100644 --- a/src/generators/model/Generator.php +++ b/src/generators/model/Generator.php @@ -308,7 +308,7 @@ public function generate() 'rules' => $this->generateRules($tableSchema), 'relations' => $tableRelations, 'relationsClassHints' => $this->generateRelationsClassHints($tableRelations, $this->generateQuery), - 'enum' => $this->getEnum($tableSchema->columns), + 'enum' => EnumGenerator::loadEnumColumns($this, $tableSchema->columns), ]; $files[] = new CodeFile( Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $modelClassName . '.php', @@ -511,9 +511,8 @@ public function generateRules($table) $rules[] = "[['" . implode("', '", $columns) . "'], 'string', 'max' => $length]"; } - $columnsEnum = $this->getEnum($table->columns); - foreach ($columnsEnum as $fieldName => $columnEnum) { - $rules['enum-' . $fieldName] = "['" . $fieldName . "', 'in', 'range' => array_keys(self::" . $columnEnum['funcOptsName'] . '())]'; + foreach (EnumGenerator::loadEnumColumns($this, $table->columns) as $columnEnum) { + $rules['enum-' . $columnEnum->getColumnsName()] = "['" . $columnEnum->getColumnsName() . "', 'in', 'range' => array_keys(self::" . $columnEnum->createOptsFunctionName() . '())]'; } $db = $this->getDbConnection(); @@ -1195,57 +1194,6 @@ protected function isColumnAutoIncremental($table, $columns) return false; } - /** - * Prepares ENUM field values. - * - * @param ColumnSchema[] $columns - * - * @return array - */ - public function getEnum($columns) - { - $enum = []; - foreach ($columns as $column) { - if (!$this->isEnum($column)) { - continue; - } - - $columnCamelName = Inflector::id2camel($column->name, '_'); - $enum[$column->name]['funcOptsName'] = 'opts' . $columnCamelName; - $enum[$column->name]['isFunctionPrefix'] = 'is' . $columnCamelName; - $enum[$column->name]['setFunctionPrefix'] = 'set' . $columnCamelName . 'To'; - $enum[$column->name]['displayFunctionPrefix'] = 'display' . $columnCamelName; - $enum[$column->name]['columnName'] = $column->name; - $enum[$column->name]['values'] = []; - - foreach ($column->enumValues as $value) { - - $constantName = strtoupper(Inflector::slug($column->name . ' ' . $value, '_')); - $label = Inflector::camel2words($value); - - $enum[$column->name]['values'][] = [ - 'value' => $value, - 'constName' => $constantName, - 'label' => $label, - 'functionSuffix' => Inflector::id2camel(Inflector::slug($value)) - ]; - } - } - - return $enum; - } - - /** - * Checks if column is of ENUM type. - * - * @param ColumnSchema $column Column instance - * @return bool - */ - protected function isEnum($column) - { - return !empty($column->enumValues) || stripos($column->dbType, 'ENUM') === 0; - } - /** * Returns the class name resolution * @param string $class diff --git a/src/generators/model/default/model.php b/src/generators/model/default/model.php index 2777f7e32..15cc01187 100644 --- a/src/generators/model/default/model.php +++ b/src/generators/model/default/model.php @@ -3,7 +3,7 @@ * This is the template for generating the model class of a specified table. */ -/** @var $enum array list of ENUM fields */ +/** @var $enum EnumGenerator[] list of ENUM fields */ /** @var yii\web\View $this */ /** @var yii\gii\generators\model\Generator $generator */ /** @var string $tableName full table name */ @@ -16,6 +16,8 @@ /** @var array $relations list of relations (name => relation declaration) */ /** @var array $relationsClassHints */ +use yii\gii\generators\model\EnumGenerator; + echo " @@ -44,9 +46,9 @@ class extends baseClass, '\\') . * ENUM field values */ $columnData) { - foreach ($columnData['values'] as $enumValue){ - echo ' const ' . $enumValue['constName'] . ' = \'' . $enumValue['value'] . '\';' . PHP_EOL; + foreach($enum as $enumColumn) { + foreach ($enumColumn->enumConstantList() as $enumConstant){ + echo ' const ' . $enumConstant['constantName'] . ' = \'' . $enumConstant['value'] . '\';' . PHP_EOL; } } endif @@ -117,49 +119,49 @@ public static function find() - $columnData): ?> + /** - * column ENUM value labels + * column getColumnsName() ?> ENUM value labels * @return string[] */ - public static function () + public static function createOptsFunctionName()?>() { return [ - $value): ?> +enumConstantList() as $enumConstantData): ?> enableI18N) { - echo ' self::' . $value['constName'] . ' => Yii::t(\'' . $generator->messageCategory . '\', \'' . $value['value'] . "'),\n"; + echo ' self::' . $enumConstantData['constantName'] . ' => Yii::t(\'' . $generator->messageCategory . '\', \'' . $enumConstantData['value'] . "'),\n"; } else { - echo ' self::' . $value['constName'] . ' => \'' . $value['value'] . "',\n"; + echo ' self::' . $enumConstantData['constantName'] . ' => \'' . $enumConstantData['value'] . "',\n"; } ?> ]; } - $columnData): ?> + /** * @return string */ - public function () + public function createDisplayFunctionName()?>() { - return self::()[$this->]; + return self::createOptsFunctionName()?>()[$this->getColumnsName()?>]; } - +enumConstantList() as $enumConstantData): ?> /** * @return bool */ - public function () + public function createIsFunctionName($enumConstantData['value'])?>() { - return $this-> === self::; + return $this->getColumnsName() ?> === self::; } - public function () + public function createSetFunctionName($enumConstantData['value'])?>() { - $this-> = self::; + $this->getColumnsName() ?> = self::; } diff --git a/tests/generators/ModelGeneratorTest.php b/tests/generators/ModelGeneratorTest.php index bb1d9f6d4..360eb244c 100644 --- a/tests/generators/ModelGeneratorTest.php +++ b/tests/generators/ModelGeneratorTest.php @@ -3,6 +3,7 @@ use yii\db\mysql\ColumnSchema; use yii\db\TableSchema; +use yii\gii\generators\model\EnumGenerator; use yii\gii\generators\model\Generator as ModelGenerator; use yiiunit\gii\GiiTestCase; @@ -504,7 +505,7 @@ public function testEnum() 'rules' => $generator->generateRules($tableSchema), 'relations' => [], 'relationsClassHints' => [], - 'enum' => $generator->getEnum($tableSchema->columns), + 'enum' => EnumGenerator::loadEnumColumns($generator, $tableSchema->columns), ]; $codeFile = $generator->render('model.php', $params); @@ -534,15 +535,73 @@ public static function getTableSchema(){ $testEnumModel = new \TestEnumModel(); $testEnumModel::$testTableSchema = $this->createEnumTableSchema(); - /** test assigning and method is... */ - $testEnumModel->type = \TestEnumModel::TYPE_CLIENT; - $this->assertTrue($testEnumModel->isTypeClient()); - $this->assertFalse($testEnumModel->isTypeConsignees()); + foreach( + [ + [ + 'value'=>'Client', + 'constant'=>'TYPE_CLIENT', + 'set'=>'setTypeToClient', + 'isSet'=>'isTypeClient', + ], + [ + 'value' => 'Consignees', + 'constant'=>'TYPE_CONSIGNEES', + 'set' => 'setTypeToConsignees', + 'isSet' => 'isTypeConsignees', + ], + [ + 'value' => 'Car cleaner', + 'constant'=>'TYPE_CAR_CLEANER', + 'set' => 'setTypeToCarCleaner', + 'isSet' => 'isTypeCarCleaner', + ], + [ + 'value' => 'B+', + 'constant'=>'TYPE_B_PLUS', + 'set' => 'setTypeToBPlus', + 'isSet' => 'isTypeBPlus', + ], + [ + 'value' => 'B-', + 'constant'=>'TYPE_B_MINUS', + 'set' => 'setTypeToBMinus', + 'isSet' => 'isTypeBMinus', + ], + [ + 'value' => 'A-Foo', + 'constant'=>'TYPE_A_FOO', + 'set' => 'setTypeToAFoo', + 'isSet' => 'isTypeAFoo', + ], + [ + 'value' => '-A', + 'constant'=>'TYPE_MINUS_A', + 'set' => 'setTypeToMinusA', + 'isSet' => 'isTypeMinusA', + ] + ] as $tesEnum + ) { + $this->assertTrue( + defined('\TestEnumModel::'.$tesEnum['constant']), + 'Constant ' . $tesEnum['constant'] . ' should be defined. ' . $classCode + ); + $this->assertTrue( + method_exists($testEnumModel, $tesEnum['set']), + 'Moethod ' . $tesEnum['set'] . ' not exist. ' . $classCode + ); + $this->assertTrue( + method_exists($testEnumModel,$tesEnum['isSet']), + 'Moethod ' . $tesEnum['isSet'] . ' not exist. ' . $classCode + ); + $testEnumModel->type = constant('\TestEnumModel::'.$tesEnum['constant']); + $this->assertTrue($testEnumModel->validate()); + $testEnumModel->{$tesEnum['set']}(); + $this->assertTrue($testEnumModel->{$tesEnum['isSet']}()); - $testEnumModel->type = \TestEnumModel::TYPE_CONSIGNEES; - $this->assertFalse($testEnumModel->isTypeClient()); - $this->assertTrue($testEnumModel->isTypeConsignees()); - $this->assertEquals(\TestEnumModel::TYPE_CONSIGNEES,$testEnumModel->displayType()); + $opts = $testEnumModel::optsType(); + $this->assertArrayHasKey($tesEnum['value'], $opts, 'Enum value ' . $tesEnum['value'] . ' should be in optsType. ' . print_r($opts, true)); + + } /** test validate */ $this->assertTrue($testEnumModel->validate()); @@ -551,6 +610,19 @@ public static function getTableSchema(){ } + public function testEnumDuplicateEnumNames() + { + $generator = new ModelGenerator(); + $generator->template = 'default'; + $generator->tableName = 'category_photo'; + $tableSchema = $this->createEnumTableSchemaDuplicateEnumConstantName(); + $enumColumns = EnumGenerator::loadEnumColumns($generator, $tableSchema->columns); + $enumColumns['type']->enumConstantList(); + $generatorErrors = $generator->errors; + $this->assertArrayHasKey('tableName', $generatorErrors, 'Enum column \'type\' has generated duplicate constant names. Error: ' . print_r($generatorErrors, true)); + $this->assertStringStartsWith("Enum column 'type' has generated duplicate constant names ", $generatorErrors['tableName'][0]); + } + public function createEnumTableSchema() { $schema = new TableSchema(); @@ -576,11 +648,57 @@ public function createEnumTableSchema() 'allowNull' => true, 'type' => 'string', 'phpType' => 'string', - 'dbType' => 'enum(\'Client\',\'Consignees\',\'Car cleaner\')', + 'dbType' => 'enum(\'Client\',\'Consignees\',\'Car cleaner\',\'B+\',\'B-\',\'A-Foo\',\'-A\')', 'enumValues' => [ 0 => 'Client', 1 => 'Consignees', 2 => 'Car cleaner', + 3 => 'B+', + 4 => 'B-', + 5 => 'A-Foo', + 6 => '-A', + ], + 'size' => null, + 'precision' => null, + 'isPrimaryKey' => false, + 'autoIncrement' => false, + 'unsigned' => false, + 'comment' => '' + ]), + ]; + + return $schema; + } + + public function createEnumTableSchemaDuplicateEnumConstantName() + { + $schema = new TableSchema(); + $schema->name = 'company_type'; + $schema->fullName = 'company_type'; + $schema->primaryKey = ['id']; + $schema->columns = [ + 'id' => new ColumnSchema([ + 'name' => 'id', + 'allowNull' => false, + 'type' => 'smallint', + 'phpType' => 'integer', + 'dbType' => 'smallint(5) unsigned', + 'size' => 5, + 'precision' => 5, + 'isPrimaryKey' => true, + 'autoIncrement' => true, + 'unsigned' => true, + 'comment' => '' + ]), + 'type' => new ColumnSchema([ + 'name' => 'type', + 'allowNull' => true, + 'type' => 'string', + 'phpType' => 'string', + 'dbType' => 'enum(\'B -\',\'B-\')', + 'enumValues' => [ + 0 => 'B -', + 1 => 'B-' ], 'size' => null, 'precision' => null,