SeederController::getDefaultSeeder()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 11
c 5
b 0
f 0
dl 0
loc 22
rs 9.9
cc 4
nc 4
nop 0
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\db\Schema;
10
use yii\helpers\ArrayHelper;
11
use yii\helpers\Console;
12
use yii\helpers\FileHelper;
13
use yii\helpers\Inflector;
14
use yii\helpers\StringHelper;
15
16
/**
17
 * Class SeederController
18
 * 
19
 * @package diecoding\seeder
20
 * 
21
 * @link [sugeng-sulistiyawan.github.io](sugeng-sulistiyawan.github.io)
22
 * @author Sugeng Sulistiyawan <[email protected]>
23
 * @copyright Copyright (c) 2023
24
 */
25
class SeederController extends Controller
26
{
27
    /** @var string the default command action. */
28
    public $defaultAction = 'seed';
29
30
    /** @var string seeder path, support path alias */
31
    public $seederPath = '@console/seeder';
32
33
    /** @var string seeder namespace */
34
    public $seederNamespace = "console\\seeder";
35
36
    /** 
37
     * @var string this class look like `$this->seederNamespace\Seeder` 
38
     * default seeder class run if no class selected, 
39
     * must instance of `\diecoding\seeder\TableSeeder` 
40
     */
41
    public $defaultSeederClass = 'Seeder';
42
43
    /** @var string tables path, support path alias */
44
    public $tablesPath = '@console/seeder/tables';
45
46
    /** @var string seeder table namespace */
47
    public $tableSeederNamespace = "console\\seeder\\tables";
48
49
    /** @var string model namespace */
50
    public $modelNamespace = "common\\models";
51
52
    /** @var string path view template table seeder, support path alias */
53
    public $templateSeederFile = __DIR__ . '/views/Seeder.php';
54
55
    /** @var string path view template seeder, support path alias */
56
    public $templateTableFile = __DIR__ . '/views/TableSeeder.php';
57
58
    /** @var bool run on production or Seeder on YII_ENV === 'prod' */
59
    public $runOnProd;
60
61
    /** @var \yii\db\ActiveRecord */
62
    protected $model = null;
63
64
    /**
65
     * {@inheritdoc}
66
     */
67
    public function options($actionID)
68
    {
69
        return ['runOnProd'];
70
    }
71
72
    /**
73
     * @inheritdoc
74
     */
75
    public function init()
76
    {
77
        parent::init();
78
79
        $this->seederPath = (string) Yii::getAlias($this->seederPath);
80
        $this->tablesPath = (string) Yii::getAlias($this->tablesPath);
81
        $this->templateSeederFile = (string) Yii::getAlias($this->templateSeederFile);
82
        $this->templateTableFile = (string) Yii::getAlias($this->templateTableFile);
83
    }
84
85
    /**
86
     * Seed action
87
     *
88
     * @param string $name
89
     * @return int ExitCode::OK
90
     */
91
    public function actionSeed($name = "")
92
    {
93
        if (YII_ENV_PROD && !$this->runOnProd) {
94
            $this->stdout("YII_ENV is set to 'prod'.\nUse seeder is not possible on production systems. use '--runOnProd' to ignore it.\n");
95
            return ExitCode::OK;
96
        }
97
98
        $explode = explode(':', $name);
99
        $name = $explode[0];
100
        $function = $explode[1] ?? null;
101
102
        if ($name) {
103
            $modelClass = str_replace('/', '\\', $name);
104
            $explode = explode('\\', $modelClass);
105
            $modelName = Inflector::camelize(array_pop($explode));
106
            $modelNamespace = implode('\\', $explode);
107
108
            $modelClass = $modelNamespace ? "{$modelNamespace}\\{$modelName}" : $modelName;
109
            $func = $function ?? 'run';
110
            $seederClasses = [
111
                $modelClass,
112
                "{$modelClass}Seeder",
113
                "{$this->seederNamespace}\\{$modelClass}",
114
                "{$this->seederNamespace}\\{$modelClass}Seeder",
115
                "{$modelClass}TableSeeder",
116
                "{$this->tableSeederNamespace}\\{$modelClass}",
117
                "{$this->tableSeederNamespace}\\{$modelClass}TableSeeder",
118
                $name,
119
                "{$name}Seeder",
120
                "{$this->seederNamespace}\\{$name}",
121
                "{$this->seederNamespace}\\{$name}Seeder",
122
                "{$name}TableSeeder",
123
                "{$this->tableSeederNamespace}\\{$name}",
124
                "{$this->tableSeederNamespace}\\{$name}TableSeeder",
125
            ];
126
127
            $this->runMethod($seederClasses, $func, $name);
128
        } else if (($defaultSeeder = $this->getDefaultSeeder()) !== null) {
129
            $defaultSeeder->run();
130
        }
131
132
        return ExitCode::OK;
133
    }
134
135
    /**
136
     * Create a new seeder.
137
     *
138
     * This command create a new seeder using the available seeder template.
139
     * After using this command, developers should modify the created seeder
140
     * skeleton by filling up the actual seeder logic.
141
     *
142
     * ```shell
143
     * yii seeder/create model_name
144
     * ```
145
     * or
146
     * ```shell
147
     * yii seeder/create modelName
148
     * ```
149
     * or
150
     * ```shell
151
     * yii seeder/create model-name
152
     * ```
153
     * 
154
     * @see https://www.yiiframework.com/doc/api/2.0/yii-helpers-inflector#camelize()-detail
155
     *
156
     * For example:
157
     *
158
     * ```shell
159
     * yii seeder/create user
160
     * ```
161
     * or
162
     * ```shell
163
     * yii seeder/create example/user
164
     * ```
165
     * if User's Model directory is "common\models\example\User", this default use `$modelNamespace` configuration
166
     * 
167
     * or you can use full path of your class name
168
     * 
169
     * ```shell
170
     * yii seeder/create \\app\\models\\User
171
     * ```
172
     * or
173
     * ```shell
174
     * yii seeder/create \\backend\\modules\\example\\models\\User
175
     * ```
176
     *
177
     * @param string $modelName the name of the new seeder or class
178
     *
179
     * @return int ExitCode::OK
180
     */
181
    public function actionCreate($modelName)
182
    {
183
        $modelName = str_replace('/', '\\', $modelName);
184
185
        $this->model = $this->getClass($modelName);
186
        if ($this->model === null) {
187
            $modelNamespace = $this->modelNamespace;
188
            $file = $this->normalizeFile($modelNamespace, $modelName);
189
190
            $this->model = $this->getClass($file);
191
            if ($this->model === null) {
192
                $this->printError("Class {$file} not exists.\n");
193
194
                return ExitCode::OK;
195
            }
196
        }
197
198
        $modelClass = get_class($this->model);
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'{$file}' 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
     * @return \yii\db\ActiveRecord|null
222
     */
223
    protected function getClass($path)
224
    {
225
        if (class_exists($path)) {
226
            return new $path;
227
        }
228
229
        return null;
230
    }
231
232
    /**
233
     * @param string $message
234
     * @param bool $print
235
     * @return void
236
     */
237
    protected function printError($message, $print = true)
238
    {
239
        if ($print) {
240
            $this->stdout($message, Console::FG_RED);
241
        }
242
    }
243
244
    /**
245
     * Generate fields for views template
246
     *
247
     * @return object
248
     */
249
    protected function generateFields()
250
    {
251
        $modelClass = get_class($this->model);
252
        $modelNamespace = str_replace('/', '\\', StringHelper::dirname($modelClass));
253
254
        $schema = $this->model->tableSchema;
255
        $columns = $schema->columns;
256
        $foreignKeys = $schema->foreignKeys;
257
        $fields = [];
258
259
        foreach ($foreignKeys as $fk_str => $foreignKey) {
260
            unset($foreignKeys[$fk_str]);
261
            $table = array_shift($foreignKey);
262
            $column = array_keys($foreignKey)[0];
263
264
            $model = $this->getClass(($class = $modelNamespace . '\\' . Inflector::camelize($table)));
265
            $foreignKeys[$column] = $model;
266
267
            $this->printError("Class {$class} not exists. Foreign Key for '$column' column will be ignored and a common column will be generated.\n", $model === null);
268
        }
269
270
        foreach ($columns as $column => $data) {
271
            /** @var ColumnSchema $data */
272
            if ($data->autoIncrement) {
273
                continue;
274
            }
275
276
            $foreign = $ref_table_id = null;
277
            if (isset($foreignKeys[$column])) {
278
                $foreign = $foreignKeys[$column];
279
                $ref_table_id = $foreign->tableSchema->primaryKey[0];
280
            }
281
282
            $faker = $this->generateFakerField($data->name, $data->type) ?? $this->generateFakerField($data->type);
283
            if ($data->dbType === 'tinyint(1)') {
284
                $faker = 'boolean';
285
            }
286
287
            $fields[$column] = (object) [
288
                'faker' => $faker,
289
                'foreign' => $foreign,
290
                'ref_table_id' => $ref_table_id
291
            ];
292
        }
293
294
        return (object) $fields;
295
    }
296
297
    /**
298
     * Generate Faker Field Name
299
     *
300
     * @param string $key
301
     * @param string|null $dataType The data type of the column schema
302
     * @return string
303
     */
304
    protected function generateFakerField($key, $dataType = null)
305
    {
306
        $faker = [
307
            'full_name' => 'name',
308
            'name' => 'name',
309
            'short_name' => 'firstName',
310
            'first_name' => 'firstName',
311
            'nickname' => 'firstName',
312
            'last_name' => 'lastName',
313
            'description' => 'realText()',
314
            'company' => 'company',
315
            'business_name' => 'company',
316
            'email' => 'email',
317
            'phone' => 'phoneNumber',
318
            'hp' => 'phoneNumber',
319
            'start_date' => 'dateTime()->format("Y-m-d H:i:s")',
320
            'end_date' => 'dateTime()->format("Y-m-d H:i:s")',
321
            'created_at' => $dataType === Schema::TYPE_INTEGER ? 'dateTime()->getTimestamp()' : 'dateTime()->format("Y-m-d H:i:s")',
322
            'updated_at' => $dataType === Schema::TYPE_INTEGER ? 'dateTime()->getTimestamp()' : 'dateTime()->format("Y-m-d H:i:s")',
323
            'token' => 'uuid',
324
            'duration' => 'numberBetween()',
325
326
            'integer' => 'numberBetween(0, 10)',
327
            'smallint' => 'numberBetween(0, 10)',
328
            'tinyint' => 'numberBetween(0, 10)',
329
            'mediumint' => 'numberBetween(0, 10)',
330
            'int' => 'numberBetween(0, 10)',
331
            'bigint' => 'numberBetween()',
332
            'date' => 'date()',
333
            'datetime' => 'dateTime()->format("Y-m-d H:i:s")',
334
            'timestamp' => 'dateTime()->format("Y-m-d H:i:s")',
335
            'year' => 'year()',
336
            'time' => 'time()',
337
        ];
338
339
        return ArrayHelper::getValue($faker, $key, 'word');
340
    }
341
342
    /**
343
     * Get Default Seeder Class if no class selected
344
     *
345
     * @return TableSeeder|null
346
     */
347
    protected function getDefaultSeeder()
348
    {
349
        $defaultSeederClass = "{$this->seederNamespace}\\{$this->defaultSeederClass}";
350
        $defaultSeederFile = "{$this->seederPath}/{$this->defaultSeederClass}.php";
351
352
        if (!class_exists($defaultSeederClass) || !file_exists($defaultSeederFile)) {
353
            FileHelper::createDirectory($this->seederPath);
354
            $content = $this->renderFile($this->templateSeederFile, [
355
                'namespace' => $this->seederNamespace,
356
            ]);
357
358
            $this->stdout("\nClass {$defaultSeederClass} created in {$defaultSeederFile}.\n");
359
360
            file_put_contents($defaultSeederFile, $content, LOCK_EX);
361
        }
362
363
        if (($seederClass = new $defaultSeederClass) instanceof TableSeeder) {
364
            /** @var TableSeeder $seederClass */
365
            return $seederClass;
366
        }
367
368
        return null;
369
    }
370
371
    /**
372
     * Execute function
373
     *
374
     * @param array|string $class
375
     * @param string $method
376
     * @param string|null $defaultClass
377
     * @return void
378
     */
379
    protected function runMethod($class, $method = 'run', $defaultClass = null)
380
    {
381
        $count = 0;
382
        foreach ((array) $class as $seederClass) {
383
            if ($seeder = $this->getClass($seederClass)) {
384
                $seeder->{$method}();
385
                $count++;
386
                break;
387
            }
388
        }
389
        $defaultClass = $defaultClass ?? $class[0];
390
        $this->printError("Class {$defaultClass} not exists.\n", $count === 0);
391
    }
392
393
    /**
394
     * @param string $modelNamespace
395
     * @param string $modelName
396
     * @return string
397
     */
398
    private function normalizeFile($modelNamespace, $modelName)
399
    {
400
        $file = "{$modelNamespace}\\{$modelName}";
401
        if (strpos($modelName, '\\') !== false) {
402
            $explode = explode('\\', $modelName);
403
            $modelName = array_pop($explode);
404
            $modelNamespace .= '\\' . implode('\\', $explode);
405
406
            $file = "{$modelNamespace}\\{$modelName}";
407
        }
408
        if (!class_exists($file)) {
409
            $modelName = Inflector::camelize($modelName);
410
            $file = "{$modelNamespace}\\{$modelName}";
411
        }
412
413
        return $file;
414
    }
415
}
416