yiisoft / yii2-gii

Yii 2 Gii Extension
http://www.yiiframework.com
BSD 3-Clause "New" or "Revised" License
202 stars 192 forks source link

Problems with Model generator #429

Open DeryabinSergey opened 4 years ago

DeryabinSergey commented 4 years ago

What steps will reproduce the problem?

At documentation Getting Started for example next SQL

CREATE TABLE `country` (
  `code` CHAR(2) NOT NULL PRIMARY KEY,
  `name` CHAR(52) NOT NULL,
  `population` INT(11) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Look at last column population - it`s not null and has default value.

Next in start gii generating Model Country with next rules

    public function rules()
    {
        return [
            [['code', 'name'], 'required'],
            [['population'], 'integer'],
            [['code'], 'string', 'max' => 2],
            [['name'], 'string', 'max' => 52],
            [['code'], 'unique'],
        ];
    }

population is not required and has no default value.

Next step in CRUD make editor. In Add object form - population is not required filed, but when it`s blank I have SQL Error

SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'population' cannot be null
The SQL being executed was: INSERT INTO `country` (`code`, `name`, `population`) VALUES ('44', '44', NULL)

What is the expected result?

For this case good SQL is without empty form field

INSERT INTO `country` (`code`, `name`) VALUES ('44', '44')

or with default value from scheme

INSERT INTO `country` (`code`, `name`, `population`) VALUES ('44', '44', '0')

Additional info

Q A
Yii version 2.0.34
PHP version 7.4.5
MySQL version 8.0.19
Operating system debian 10
fcaldarelli commented 4 years ago

Without making changes to Yii source code, you should solve adding a new rule based on default validator:

['population', 'default', 'value' => 0],

to be used only with NOT NULL field but with a DEFAULT value.

Next, Gii generator could be updated to produces this rule.

DeryabinSergey commented 4 years ago

Ок, one more case. If field UNSIGNED

`population` int unsigned NOT NULL DEFAULT '0'

by default generator no rule fo this. I can put -1 in form and have error

SQLSTATE[22003]: Numeric value out of range: 1264 Out of range value for column 'population' at row 1
The SQL being executed was: INSERT INTO `country` (`code`, `name`, `population`) VALUES ('44', '44', -10)
fcaldarelli commented 4 years ago

I have quickly read yii2 gii source code and found where rules are created:

https://github.com/yiisoft/yii2-gii/blob/aa7a821f5839b619bf820dcfe3738e011337cfa7/src/generators/model/Generator.php#L378

and there is not specific code for unsigned type.

I'll make a PR updating this part to support default and unsgined values.

uldisn commented 4 years ago

Generate advanced rules:

public function generateRules($table)
    {
        $columns = [];
        foreach ($table->columns as $index => $column) {
            $isBlameableCol = ($column->name === $this->createdByColumn || $column->name === $this->updatedByColumn);
            $isTimestampCol = ($column->name === $this->createdAtColumn || $column->name === $this->updatedAtColumn);
            $removeCol = ($this->useBlameableBehavior && $isBlameableCol)
                || ($this->useTimestampBehavior && $isTimestampCol);
            if ($removeCol) {
                $columns[$index] = $column;
                unset($table->columns[$index]);
            }
        }

        $rules = [];

        //for enum fields create rules "in range" for all enum values
        $enum = $this->getEnum($table->columns);
        foreach ($enum as $field_name => $field_details) {
            $ea = array();
            foreach ($field_details['values'] as $field_enum_values) {
                $ea[] = 'self::'.$field_enum_values['const_name'];
            }
            $rules['enum-' . $field_name] = "['".$field_name."', 'in', 'range' => [\n                    ".implode(
                    ",\n                    ",
                    $ea
                ).",\n                ]\n            ]";
        }

        $rules = array_merge($rules, $this->rulesIntegerMinMax($table));

        // inject namespace for targetClass
        $modTable = clone $table;
        $intColumn = 'nonecolumn';
        foreach($modTable->columns as $key => $column){
            switch ($column->type) {
                case Schema::TYPE_SMALLINT:
                case Schema::TYPE_INTEGER:
                case Schema::TYPE_BIGINT:
                case Schema::TYPE_TINYINT:
                case 'mediumint':
                    $modTable->columns[$key]->type = Schema::TYPE_INTEGER;
                $intColumn = $column->name;
            }
        }
        $parentRules = parent::generateRules($modTable);
        $ns = "\\{$this->ns}\\";
        $match = "'targetClass' => ";
        $replace = $match.$ns;
        foreach ($parentRules as $k => $parentRule) {
            if(preg_match('#\'' . $intColumn . '\',.*\'string\', \'max\' => 5#',$parentRule)){
                unset($parentRules[$k]);
                continue;
            }
            if(preg_match('#\'integer\']$#',$parentRule)){
                unset($parentRules[$k]);
                continue;
            }
            $parentRules[$k] = str_replace($match, $replace, $parentRule);
        }

        $rules = array_merge($rules,$parentRules);
        $table->columns = array_merge($table->columns, $columns);

        return $rules;
    }
    /**
     * Generates validation min max rules.
     *
     * @param TableSchema $table the table schema
     *
     * @return array the generated validation rules
     */
    public function rulesIntegerMinMax($table): array
    {
        $UNSIGNED = 'Unsigned';
        $SIGNED = 'Signed';
        $rules = [
            Schema::TYPE_TINYINT . ' ' . $UNSIGNED => [
                [],
                'integer',
                'min' => 0,
                'max' => 255,
            ],
            Schema::TYPE_TINYINT . ' ' . $SIGNED => [
                [],
                'integer',
                'min' => -128,
                'max' => 127,
            ],
            Schema::TYPE_SMALLINT . ' ' . $UNSIGNED => [
                [],
                'integer',
                'min' => 0,
                'max' => 65535,
            ],
            Schema::TYPE_SMALLINT . ' ' . $SIGNED => [
                [],
                'integer',
                'min' => -32768,
                'max' => 32767,
            ],
            'mediumint' . ' ' . $UNSIGNED => [
                [],
                'integer',
                'min' => 0,
                'max' => 16777215,
            ],
            'mediumint' . ' ' . $SIGNED => [
                [],
                'integer',
                'min' => -8388608,
                'max' => 8388607,
            ],
            Schema::TYPE_INTEGER . ' ' . $UNSIGNED => [
                [],
                'integer',
                'min' => 0,
                'max' => 4294967295,
            ],
            Schema::TYPE_INTEGER . ' ' . $SIGNED => [
                [],
                'integer',
                'min' => -2147483648,
                'max' => 2147483647,
            ],
            Schema::TYPE_BIGINT . ' ' . $UNSIGNED => [
                [],
                'integer',
                'min' => 0,
                'max' => 0xFFFFFFFFFFFFFFFF,
            ],
            Schema::TYPE_BIGINT . ' ' . $SIGNED => [
                [],
                'integer',
                'min' => -0xFFFFFFFFFFFFFFF,
                'max' => 0xFFFFFFFFFFFFFFE,
            ],

        ];

        foreach ($table->columns as $column) {
            $key = $column->type . ' ' . ($column->unsigned? $UNSIGNED : $SIGNED);
            if(!isset($rules[$key])){
                continue;
            }

            $rules[$key][0][] = $column->name;
        }

        /**
         * remove empty rules
         */
        foreach($rules as $ruleName => $rule){
            if(!$rule[0]){
                unset($rules[$ruleName]);
                continue;
            }
            $rules[$ruleName] = '['
                . '[\'' . implode('\',\'',$rule[0]) . '\']'
                . ',\'integer\''
                . ' ,\'min\' => ' . $rule['min']
                . ' ,\'max\' => ' . $rule['max']
                . ']';
        }

        return $rules;

    }
fcaldarelli commented 4 years ago

Generate advanced rules:


public function generateRules($table)

Is this code available in some branch (to be tested) ?

uldisn commented 4 years ago

Generate advanced rules:

public function generateRules($table)

Is this code available in some branch (to be tested) ?

It is in my private repository. Can give access.

fcaldarelli commented 4 years ago

It is in my private repository. Can give access.

Great, so you could create a PR starting from your local branch.

Have you tried to launch the tests suite?

uldisn commented 4 years ago

It is in my private repository. Can give access.

Great, so you could create a PR starting from your local branch.

Have you tried to launch the tests suite?

Check email. You are free to use the code to top up your Gii. I don't have time now.

DeryabinSergey commented 4 years ago

@FabrizioCaldarelli maybe mediumint shoud add to Schema constants if use @uldisn case

uldisn commented 4 years ago

Additionaly can add enum:

generator.ph

'enum' => $this->getEnum($tableSchema->columns),
//..........................

    /**
     * prepare ENUM field values.
     *
     * @param array $columns
     *
     * @return array
     */
    public function getEnum($columns)
    {
        $enum = [];
        foreach ($columns as $column) {
            if (!$this->isEnum($column)) {
                continue;
            }

            $column_camel_name = str_replace(' ', '', ucwords(implode(' ', explode('_', $column->name))));
            $enum[$column->name]['func_opts_name'] = 'opts'.$column_camel_name;
            $enum[$column->name]['func_get_label_name'] = 'get'.$column_camel_name.'ValueLabel';
            $enum[$column->name]['isFunctionPrefix'] = 'is'.$column_camel_name;
            $enum[$column->name]['columnName'] = $column->name;
            $enum[$column->name]['values'] = [];

            $enum_values = explode(',', substr($column->dbType, 4, strlen($column->dbType) - 1));

            foreach ($enum_values as $value) {
                $value = trim($value, "()'");

                $const_name = strtoupper($column->name.'_'.$value);
                $const_name = preg_replace('/\s+/', '_', $const_name);
                $const_name = str_replace(['-', '_', ' '], '_', $const_name);
                $const_name = preg_replace('/[^A-Z0-9_]/', '', $const_name);

                $label = Inflector::camel2words($value);

                $enum[$column->name]['values'][] = [
                    'value' => $value,
                    'const_name' => $const_name,
                    'label' => $label,
                    'isFunctionSuffix' => str_replace(' ', '', ucwords(implode(' ', explode('_', $value))))
                ];
            }
        }

        return $enum;
    }

    /**
     * validate is ENUM.
     *
     * @param  $column table column
     *
     * @return type
     */
    public function isEnum($column)
    {
        return substr(strtoupper($column->dbType), 0, 4) == 'ENUM';
    }

template
<?php
if(!empty($enum)){
?>
    /**
    * ENUM field values
    */
<?php
    foreach($enum as $column_name => $column_data){
        foreach ($column_data['values'] as $enum_value){
            echo '    public const ' . $enum_value['const_name'] . ' = \'' . $enum_value['value'] . '\';' . PHP_EOL;
        }
    }
}

//................

    foreach($enum as $column_name => $column_data){
?>

    /**
     * get column <?php echo $column_name?> enum value label
     * @param string $value
     * @return string
     */
    public static function <?php echo $column_data['func_get_label_name']?>($value): string
    {
        if(!$value){
            return '';
        }
        $labels = self::<?php echo $column_data['func_opts_name']?>();
        return $labels[$value] ?? $value;
    }

    /**
     * column <?php echo $column_name?> ENUM value labels
     * @return array
     */
    public static function <?php echo $column_data['func_opts_name']?>(): array
    {
        return [
<?php
        foreach($column_data['values'] as $k => $value){
            if ($generator->enableI18N) {
                echo '            '.'self::' . $value['const_name'] . ' => Yii::t(\'' . $generator->messageCategory . '\', \'' . $value['value'] . "'),\n";
            } else {
                echo '            '.'self::' . $value['const_name'] . ' => \'' . $column_data['columnName'] . "',\n";
            }
        }
?>
        ];
    }
<?php
    }

if(!empty($enum)){
?>
    /**
    * ENUM field values
    */
<?php
    foreach($enum as $column_name => $column_data){
        foreach ($column_data['values'] as $enum_value){
?>
    /**
     * @return bool
     */
    public function <?=$column_data['isFunctionPrefix'].$enum_value['isFunctionSuffix']?>(): bool
    {
        return $this-><?=$column_name?> === self::<?=$enum_value['const_name']?>;
    }
<?php
        }
    }
}