Passed
Push — main ( 676fd4...cd768a )
by Sugeng
02:18
created

SeederController   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 327
Duplicated Lines 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 30
eloc 138
c 6
b 0
f 0
dl 0
loc 327
rs 10

8 Methods

Rating   Name   Duplication   Size   Complexity  
B actionCreate() 0 45 8
A getDefaultSeeder() 0 22 4
B generateFields() 0 46 6
B actionSeed() 0 34 7
A getClass() 0 8 2
A init() 0 8 1
A generateFakerField() 0 29 1
A options() 0 3 1
1
<?php
2
3
namespace diecoding\seeder;
4
5
use Yii;
6
use yii\console\Controller;
7
use yii\console\ExitCode;
8
use yii\db\ColumnSchema;
9
use yii\helpers\ArrayHelper;
10
use yii\helpers\Console;
11
use yii\helpers\FileHelper;
12
use yii\helpers\Inflector;
13
use yii\helpers\StringHelper;
14
15
/**
16
 * Class SeederController
17
 * 
18
 * @package diecoding\seeder
19
 * 
20
 * @link [sugeng-sulistiyawan.github.io](sugeng-sulistiyawan.github.io)
21
 * @author Sugeng Sulistiyawan <[email protected]>
22
 * @copyright Copyright (c) 2023
23
 */
24
class SeederController extends Controller
25
{
26
    /** @var string the default command action. */
27
    public $defaultAction = 'seed';
28
29
    /** @var string seeder path, support path alias */
30
    public $seederPath = '@console/seeder';
31
32
    /** @var string seeder namespace */
33
    public $seederNamespace = 'console\seeder';
34
35
    /** 
36
     * @var string this class look like `$this->seederNamespace\Seeder` 
37
     * default seeder class run if no class selected, 
38
     * must instance of `\diecoding\seeder\TableSeeder` 
39
     */
40
    public $defaultSeederClass = 'Seeder';
41
42
    /** @var string tables path, support path alias */
43
    public $tablesPath = '@console/seeder/tables';
44
45
    /** @var string seeder table namespace */
46
    public $tableSeederNamespace = 'console\seeder\tables';
47
48
    /** @var string model namespace */
49
    public $modelNamespace = 'common\models';
50
51
    /** @var string path view template table seeder, support path alias */
52
    public $templateSeederFile = '@diecoding/seeder/views/Seeder.php';
53
54
    /** @var string path view template seeder, support path alias */
55
    public $templateTableFile = '@diecoding/seeder/views/TableSeeder.php';
56
57
    /** @var bool run on production or Seeder on YII_ENV === 'prod' */
58
    public $runOnProd;
59
60
    /** @var \yii\db\ActiveRecord */
61
    protected $model = null;
62
63
    /**
64
     * {@inheritdoc}
65
     */
66
    public function options($actionID)
67
    {
68
        return ['runOnProd'];
69
    }
70
71
    /**
72
     * @inheritdoc
73
     */
74
    public function init()
75
    {
76
        parent::init();
77
78
        $this->seederPath         = (string) Yii::getAlias($this->seederPath);
79
        $this->tablesPath         = (string) Yii::getAlias($this->tablesPath);
80
        $this->templateSeederFile = (string) Yii::getAlias($this->templateSeederFile);
81
        $this->templateTableFile  = (string) Yii::getAlias($this->templateTableFile);
82
    }
83
84
    /**
85
     * Seed action
86
     *
87
     * @param string $name
88
     * @return int ExitCode::OK
89
     */
90
    public function actionSeed($name = "")
91
    {
92
        if (YII_ENV_PROD && !$this->runOnProd) {
93
            $this->stdout("YII_ENV is set to 'prod'.\nUse seeder is not possible on production systems. use '--runOnProd' to ignore it.\n");
94
            return ExitCode::OK;
95
        }
96
97
        $explode  = explode(':', $name);
98
        $name     = $explode[0];
99
        $function = $explode[1] ?? null;
100
101
        if ($name) {
102
            $func = $function ?? 'run';
103
104
            $seederClasses = [
105
                $name,
106
                "{$name}TableSeeder",
107
                "{$this->seederNamespace}\\{$name}",
108
                "{$this->seederNamespace}\\{$name}TableSeeder",
109
                "{$this->tableSeederNamespace}\\{$name}",
110
                "{$this->tableSeederNamespace}\\{$name}TableSeeder",
111
            ];
112
113
            foreach ($seederClasses as $seederClass) {
114
                if ($seeder = $this->getClass($seederClass)) {
115
                    $seeder->{$func}();
116
                    break;
117
                }
118
            }
119
        } else if (($defaultSeeder = $this->getDefaultSeeder()) !== null) {
120
            $defaultSeeder->run();
121
        }
122
123
        return ExitCode::OK;
124
    }
125
126
    /**
127
     * Create a new seeder.
128
     *
129
     * This command create a new seeder using the available seeder template.
130
     * After using this command, developers should modify the created seeder
131
     * skeleton by filling up the actual seeder logic.
132
     *
133
     * ```shell
134
     * yii seeder/create model_name
135
     * ```
136
     * or
137
     * ```shell
138
     * yii seeder/create modelName
139
     * ```
140
     * or
141
     * ```shell
142
     * yii seeder/create model-name
143
     * ```
144
     * 
145
     * @see https://www.yiiframework.com/doc/api/2.0/yii-helpers-inflector#camelize()-detail
146
     *
147
     * For example:
148
     *
149
     * ```shell
150
     * yii seeder/create user
151
     * ```
152
     * or
153
     * ```shell
154
     * yii seeder/create example/user
155
     * ```
156
     * if User's Model directory is "common\models\example\User", this default use `$modelNamespace` configuration
157
     * 
158
     * or you can use full path of your class name
159
     * 
160
     * ```shell
161
     * yii seeder/create \app\models\User
162
     * ```
163
     * or
164
     * ```shell
165
     * yii seeder/create \backend\modules\example\models\User
166
     * ```
167
     *
168
     * @param string $modelName the name of the new seeder or class
169
     *
170
     * @return int ExitCode::OK
171
     */
172
    public function actionCreate($modelName)
173
    {
174
        $modelName = str_replace('/', '\\', $modelName);
175
176
        $this->model = $this->getClass($modelName);
177
        if ($this->model === null) {
178
            $modelNamespace = $this->modelNamespace;
179
            $file           = "{$modelNamespace}\\{$modelName}";
180
            if (strpos($modelName, '\\')) {
181
                $explode         = explode('\\', $modelName);
182
                $modelName       = array_pop($explode);
183
                $modelNamespace .= '\\' . implode('\\', $explode);
184
185
                $file = "{$modelNamespace}\\{$modelName}";
186
            }
187
            if (!class_exists($file)) {
188
                $modelName = Inflector::camelize($modelName);
189
                $file      = "{$modelNamespace}\\{$modelName}";
190
            }
191
192
            $this->model = $this->getClass($file);
193
            if ($this->model === null) {
194
                return ExitCode::OK;
195
            }
196
        }
197
198
        $modelClass = $this->model::class;
199
        $className  = StringHelper::basename($modelClass) . 'TableSeeder';
200
        $file       = "{$this->tablesPath}/{$className}.php";
201
        if ($this->confirm("Create new seeder '{$file}'?")) {
202
            $content = $this->renderFile($this->templateTableFile, [
203
                'className' => $className,
204
                'namespace' => $this->tableSeederNamespace,
205
                'model'     => $this->model,
206
                'fields'    => $this->generateFields(),
207
            ]);
208
            FileHelper::createDirectory($this->tablesPath);
209
210
            if (!file_exists($file) || $this->confirm("\n'{$className}' already exists, overwrite?\nAll data will be lost irreversibly!")) {
211
                file_put_contents($file, $content, LOCK_EX);
212
                $this->stdout("New seeder created successfully.\n", Console::FG_GREEN);
213
            }
214
        }
215
216
        return ExitCode::OK;
217
    }
218
219
    /**
220
     * @param string $path
221
     * @param string $eol
222
     * @return \yii\db\ActiveRecord|null
223
     */
224
    protected function getClass($path, $eol = PHP_EOL)
225
    {
226
        if (class_exists($path)) {
227
            return new $path;
228
        }
229
230
        $this->stdout("Class {$path} not exists. {$eol}");
231
        return null;
232
    }
233
234
    /**
235
     * Generate fields for views template
236
     *
237
     * @return object
238
     */
239
    protected function generateFields()
240
    {
241
        $modelClass     = $this->model::class;
242
        $modelNamespace = str_replace('/', '\\', StringHelper::dirname($modelClass));
243
244
        $schema      = $this->model->tableSchema;
245
        $columns     = $schema->columns;
246
        $foreignKeys = $schema->foreignKeys;
247
        $fields      = [];
248
249
        foreach ($foreignKeys as $fk_str => $foreignKey) {
250
            unset($foreignKeys[$fk_str]);
251
            $table  = array_shift($foreignKey);
252
            $column = array_keys($foreignKey)[0];
253
254
            $errorMsg = "Foreign Key for '$column' column will be ignored and a common column will be generated.\n";
255
256
            $model = $this->getClass($modelNamespace . '\\' . Inflector::camelize($table), $errorMsg);
257
            $foreignKeys[$column] = $model;
258
        }
259
260
        foreach ($columns as $column => $data) {
261
            /** @var ColumnSchema $data */
262
            if ($data->autoIncrement) {
263
                continue;
264
            }
265
266
            $foreign = $ref_table_id = null;
267
            if (isset($foreignKeys[$column])) {
268
                $foreign      = $foreignKeys[$column];
269
                $ref_table_id = $foreign->tableSchema->primaryKey[0];
270
            }
271
272
            $faker = $this->generateFakerField($data->name) ?? $this->generateFakerField($data->type);
273
            if ($data->dbType === 'tinyint(1)') {
274
                $faker = 'boolean';
275
            }
276
277
            $fields[$column] = (object) [
278
                'faker'        => $faker,
279
                'foreign'      => $foreign,
280
                'ref_table_id' => $ref_table_id
281
            ];
282
        }
283
284
        return (object) $fields;
285
    }
286
287
    /**
288
     * Generate Faker Field Name
289
     *
290
     * @param string $key
291
     * @return string
292
     */
293
    protected function generateFakerField($key)
294
    {
295
        $faker = [
296
            'full_name'     => 'name',
297
            'name'          => 'name',
298
            'short_name'    => 'firstName',
299
            'first_name'    => 'firstName',
300
            'nickname'      => 'firstName',
301
            'last_name'     => 'lastName',
302
            'description'   => 'realText()',
303
            'company'       => 'company',
304
            'business_name' => 'company',
305
            'email'         => 'email',
306
            'phone'         => 'phoneNumber',
307
            'hp'            => 'phoneNumber',
308
            'integer'       => 'numberBetween(0, 10)',
309
            'smallint'      => 'numberBetween(0, 10)',
310
            'tinyint'       => 'numberBetween(0, 10)',
311
            'mediumint'     => 'numberBetween(0, 10)',
312
            'int'           => 'numberBetween(0, 10)',
313
            'bigint'        => 'numberBetween(0, 10)',
314
            'date'          => 'date()',
315
            'datetime'      => 'dateTime()',
316
            'timestamp'     => 'dateTime()',
317
            'year'          => 'year()',
318
            'time'          => 'time()',
319
        ];
320
321
        return ArrayHelper::getValue($faker, $key, 'text');
322
    }
323
324
    /**
325
     * Get Default Seeder Class if no class selected
326
     *
327
     * @return TableSeeder|null
328
     */
329
    protected function getDefaultSeeder()
330
    {
331
        $defaultSeederClass = "{$this->seederNamespace}\\{$this->defaultSeederClass}";
332
        $defaultSeederFile = "{$defaultSeederClass}.php";
333
334
        if (!class_exists($defaultSeederClass) || !file_exists($defaultSeederFile)) {
335
            FileHelper::createDirectory($this->seederPath);
336
            $content = $this->renderFile($this->templateSeederFile, [
337
                'namespace' => $this->seederNamespace,
338
            ]);
339
340
            $this->stdout("\nClass {$defaultSeederClass} created in {$defaultSeederFile}.\n");
341
342
            file_put_contents($defaultSeederFile, $content, LOCK_EX);
343
        }
344
345
        if (($seederClass = new $defaultSeederClass) instanceof TableSeeder) {
346
            /** @var TableSeeder $seederClass */
347
            return $seederClass;
348
        }
349
350
        return null;
351
    }
352
}
353