Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Problems with Model generator #429

Open
DeryabinSergey opened this issue Apr 21, 2020 · 10 comments
Open

Problems with Model generator #429

DeryabinSergey opened this issue Apr 21, 2020 · 10 comments
Labels
type:enhancement Enhancement

Comments

@DeryabinSergey
Copy link

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
Copy link
Member

fcaldarelli commented Apr 21, 2020

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
Copy link
Author

Ок, 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
Copy link
Member

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

public function generateRules($table)

and there is not specific code for unsigned type.

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

@uldisn
Copy link
Contributor

uldisn commented Apr 22, 2020

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
Copy link
Member

Generate advanced rules:

public function generateRules($table)

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

@uldisn
Copy link
Contributor

uldisn commented Apr 22, 2020

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
Copy link
Member

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
Copy link
Contributor

uldisn commented Apr 22, 2020

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
Copy link
Author

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

@uldisn
Copy link
Contributor

uldisn commented Apr 22, 2020

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
        }
    }
}

@samdark samdark transferred this issue from yiisoft/yii2 Apr 22, 2020
@samdark samdark added the type:enhancement Enhancement label Apr 22, 2020
@samdark samdark changed the title Generating Models in Gii Problems with Model generator Apr 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type:enhancement Enhancement
Projects
None yet
Development

No branches or pull requests

4 participants