yiisoft / yii2-gii

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

Gii enhancement idea for model-generator #494

Open rackycz opened 2 years ago

rackycz commented 2 years ago

Hi. Sometimes it might be handy to split model into 2 classes: 1) class UserBase extends \yii\db\ActiveRecord - contains all code generated by Gii (placed in folder "models/base") 2) class User extends UserBase (placed in standard folder "models" and is empty at the beginning)

UserBase contains generated code while User contains code that was produced by the programmer. The programmer can also decide to use pure PHP in model User while in UserBase can be Yii-dependent code. But it is not a rule. Code then can be more readable as classes are shorter etc.

Programmer still uses the class User as usually. No changes here.

So I am suggesting this as a new option (checkbox) in Gii.

My solution:

I extended yii\gii\generators\model\Generator and following is the result. Only method generate() was changed a little. See $this->splitModelIntoBaseClass below. (In comments are also mentioned all JS and form changes)

If you think this might be usefull for others, you can check/use my code below. But it is nothing special. Just an idea.

Good luck to yo,u Yii.

PS: Gii is amazing. When I compare it to other frameworks, you are ahead by miles as Gii is delivered directly by Yii and one has all functionalities in one bundle.

image

<?php
namespace app\gii\generators\model;

use Yii;
use yii\db\ActiveRecord;
use yii\gii\CodeFile;

class Generator extends yii\gii\generators\model\Generator
{

/*
Following should be present in file yii-basic\vendor\yiisoft\yii2-gii\src\generators\model\form.php

echo $form->field($generator, 'splitModelIntoBaseClass')->checkbox(['onclick' => "$('div#splitModelIntoBaseClass').toggle($(this).is(':checked'))"]);
$displayBaseInputs = $generator->splitModelIntoBaseClass?'block':'none';
echo '<div id="splitModelIntoBaseClass" style="color: #6c757d;background-color: rgb(240,240,240);padding:1rem;border-radius: 0.25rem;display:'.$displayBaseInputs.';">';
echo $form->field($generator, 'baseModelClass');
echo $form->field($generator, 'baseModelNamespace');
echo '</div>';

$this->registerJs(
  '$("input#generator-modelclass").change(function(){ $("input#generator-basemodelclass").val($(this).val() + "Base"); $("input#generator-queryclass").val($(this).val() + "Query"); })',
  4,
  'my-button-handler'
);
*/

  public $baseModelClass = '';
  public $baseModelNamespace = 'app\models\base';
  public $splitModelIntoBaseClass = true;

  public function getName()
  {
    return 'My Model Generator';
  }

  public function hints()
  {
    return array_merge(parent::hints(), [
      'splitModelIntoBaseClass' => 'Code of the model will be split into 2 files: <strong>models/base/TableNameBase.php</strong> and <strong>models/TableName.php</strong>. The former contains all Gii-generated code while the latter EXTENDS IT and is meant for your (possibly pure PHP) code. Now it will be empty, but you can still use it as the standard model. ',

    ]);
  }

  public function attributeLabels()
  {
    return array_merge(parent::attributeLabels(), [
      'baseModelNamespace' => 'Namespace (folder) of the base class',
    ]);
  }

  // rules were copied 1:1 from the parent class, only attribute names were changed

  public function rules()
  {
    $result = array_merge(parent::rules(), [
      [['baseModelNamespace', 'baseModelClass'], 'filter', 'filter' => 'trim'],
      [['baseModelNamespace'], 'filter', 'filter' => function ($value) { return trim($value, '\\'); }],

      [['baseModelNamespace'], 'required'],
      [['baseModelClass'], 'match', 'pattern' => '/^\w+$/', 'message' => 'Only word characters are allowed.'],
      [['baseModelNamespace'], 'match', 'pattern' => '/^[\w\\\\]+$/', 'message' => 'Only word characters and backslashes are allowed.'],
      [['baseModelNamespace'], 'validateNamespace'],
      [['baseModelClass'], 'validateModelClass', 'skipOnEmpty' => false],
      [['baseClass'], 'validateClass', 'params' => ['extends' => ActiveRecord::className()]],

      [['splitModelIntoBaseClass'], 'boolean'],
    ]);
    return $result;
  }

  public function generate()
  {
    $files = [];
    $relations = $this->generateRelations();
    $db = $this->getDbConnection();
    foreach ($this->getTableNames() as $tableName) {
      // model :
      $modelClassName = $this->generateClassName($tableName);
      $queryClassName = ($this->generateQuery) ? $this->generateQueryClassName($modelClassName) : false;
      $tableRelations = isset($relations[$tableName]) ? $relations[$tableName] : [];
      $tableSchema = $db->getTableSchema($tableName);
      $params = [
        'tableName' => $tableName,
        'className' => $modelClassName,
        'queryClassName' => $queryClassName,
        'tableSchema' => $tableSchema,
        'properties' => $this->generateProperties($tableSchema),
        'labels' => $this->generateLabels($tableSchema),
        'rules' => $this->generateRules($tableSchema),
        'relations' => $tableRelations,
        'relationsClassHints' => $this->generateRelationsClassHints($tableRelations, $this->generateQuery),
      ];

      if ($this->splitModelIntoBaseClass) {
        // New code:
        $params['className'] = $this->baseModelClass;

        $origNs = $this->ns; // $this->ns is hardcoded in model.php and I didnt want to modify it, but special value in $params would be nice.
        $this->ns = $this->baseModelNamespace; // temporary change of $this->ns

        $files[] = new CodeFile(
          Yii::getAlias('@' . str_replace('\\', '/', $this->baseModelNamespace)) . '/' . $this->baseModelClass . '.php',
          $this->render('model.php', $params)
        );

        $this->ns = $origNs; // $this->ns is hardcoded in baseModel.php and I didnt want to modify it

        $files[] = new CodeFile(
          Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $modelClassName . '.php',
          $this->render('modelEmpty.php', [
            'namespace' => $this->ns,
            'className' => $modelClassName,
            'baseClass' => $this->baseModelClass,
            'baseClassNamespace' => $this->baseModelNamespace,
          ])
        );
/* modelEmpty.php contains only this empty class definition:
<?php echo '<?php'; ?>
namespace <?= $namespace ?>;
use <?= $baseClassNamespace ?>\<?= $baseClass ?>;
class <?= $className ?> extends <?= $baseClass ?> {
}
*/
      } else {
        // Original code by Yii:
        $files[] = new CodeFile(
          Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $modelClassName . '.php',
          $this->render('model.php', $params)
        );
      }

      // query :
      if ($queryClassName) {
        $params['className'] = $queryClassName;
        $params['modelClassName'] = $modelClassName;
        $files[] = new CodeFile(
          Yii::getAlias('@' . str_replace('\\', '/', $this->queryNs)) . '/' . $queryClassName . '.php',
          $this->render('query.php', $params)
        );
      }
    }

    return $files;
  }

}
samdark commented 2 years ago

@yiisoft/reviewers need your opinion.

schmunk42 commented 2 years ago

Was one reason for me to create giiant, back then :) https://github.com/schmunk42/yii2-giiant

bizley commented 2 years ago

Yii 2 is quite oldschool regarding the inheritance vs composition debate so adding additional layer of inheritance would not be a terrible thing but we all know that it's not a good thing, right? ;)

schmunk42 commented 2 years ago

Another option would be to move the generated code into Traits and just have a more or less empty class using them.

machour commented 2 years ago

I remember @cebe talking about a new version that would patch the file while keeping your changes, right Carsten?

uldisn commented 2 years ago

Thanks @schmunk42 for the base model idea in giiant. Base model regeneration after modifying a database tables is something I do daily. In addition saving gii form data for reuse would be fantastic. In Giiant it is realized and useful.

cebe commented 2 years ago

I remember @cebe talking about a new version that would patch the file while keeping your changes, right Carsten?

yeah, that's an idea which would be really cool to implement, but I had no time to play with that.