Passed
Push — main ( 72ec96...a510f3 )
by Sugeng
02:10
created

SeederController::normalizeFile()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
c 1
b 0
f 0
dl 0
loc 16
rs 9.9332
cc 3
nc 4
nop 2
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
            $modelClass     = str_replace('/', '\\', $name);
103
            $explode        = explode('\\', $modelClass);
104
            $modelName      = Inflector::camelize(array_pop($explode));
105
            $modelNamespace = implode('\\', $explode);
106
107
            $modelClass    = $modelNamespace ? "{$modelNamespace}\\{$modelName}" : $modelName;
108
            $func          = $function ?? 'run';
109
            $seederClasses = [
110
                $modelClass,
111
                "{$modelClass}TableSeeder",
112
                "{$this->seederNamespace}\\{$modelClass}",
113
                "{$this->seederNamespace}\\{$modelClass}TableSeeder",
114
                "{$this->tableSeederNamespace}\\{$modelClass}",
115
                "{$this->tableSeederNamespace}\\{$modelClass}TableSeeder",
116
                $name,
117
                "{$name}TableSeeder",
118
                "{$this->seederNamespace}\\{$name}",
119
                "{$this->seederNamespace}\\{$name}TableSeeder",
120
                "{$this->tableSeederNamespace}\\{$name}",
121
                "{$this->tableSeederNamespace}\\{$name}TableSeeder",
122
            ];
123
124
            $this->runMethod($seederClasses, $func, $name);
125
        } else if (($defaultSeeder = $this->getDefaultSeeder()) !== null) {
126
            $defaultSeeder->run();
127
        }
128
129
        return ExitCode::OK;
130
    }
131
132
    /**
133
     * Create a new seeder.
134
     *
135
     * This command create a new seeder using the available seeder template.
136
     * After using this command, developers should modify the created seeder
137
     * skeleton by filling up the actual seeder logic.
138
     *
139
     * ```shell
140
     * yii seeder/create model_name
141
     * ```
142
     * or
143
     * ```shell
144
     * yii seeder/create modelName
145
     * ```
146
     * or
147
     * ```shell
148
     * yii seeder/create model-name
149
     * ```
150
     * 
151
     * @see https://www.yiiframework.com/doc/api/2.0/yii-helpers-inflector#camelize()-detail
152
     *
153
     * For example:
154
     *
155
     * ```shell
156
     * yii seeder/create user
157
     * ```
158
     * or
159
     * ```shell
160
     * yii seeder/create example/user
161
     * ```
162
     * if User's Model directory is "common\models\example\User", this default use `$modelNamespace` configuration
163
     * 
164
     * or you can use full path of your class name
165
     * 
166
     * ```shell
167
     * yii seeder/create \app\models\User
168
     * ```
169
     * or
170
     * ```shell
171
     * yii seeder/create \backend\modules\example\models\User
172
     * ```
173
     *
174
     * @param string $modelName the name of the new seeder or class
175
     *
176
     * @return int ExitCode::OK
177
     */
178
    public function actionCreate($modelName)
179
    {
180
        $modelName = str_replace('/', '\\', $modelName);
181
182
        $this->model = $this->getClass($modelName);
183
        if ($this->model === null) {
184
            $modelNamespace = $this->modelNamespace;
185
            $file           = $this->normalizeFile($modelNamespace, $modelName);
186
187
            $this->model = $this->getClass($file);
188
            if ($this->model === null) {
189
                $this->printError("Class {$file} not exists.\n");
190
191
                return ExitCode::OK;
192
            }
193
        }
194
195
        $modelClass = $this->model::class;
196
        $className  = StringHelper::basename($modelClass) . 'TableSeeder';
197
        $file       = "{$this->tablesPath}/{$className}.php";
198
        if ($this->confirm("Create new seeder '{$file}'?")) {
199
            $content = $this->renderFile($this->templateTableFile, [
200
                'className' => $className,
201
                'namespace' => $this->tableSeederNamespace,
202
                'model'     => $this->model,
203
                'fields'    => $this->generateFields(),
204
            ]);
205
            FileHelper::createDirectory($this->tablesPath);
206
207
            if (!file_exists($file) || $this->confirm("\n'{$file}' already exists, overwrite?\nAll data will be lost irreversibly!")) {
208
                file_put_contents($file, $content, LOCK_EX);
209
                $this->stdout("New seeder created successfully.\n", Console::FG_GREEN);
210
            }
211
        }
212
213
        return ExitCode::OK;
214
    }
215
216
    /**
217
     * @param string $path
218
     * @return \yii\db\ActiveRecord|null
219
     */
220
    protected function getClass($path)
221
    {
222
        if (class_exists($path)) {
223
            return new $path;
224
        }
225
226
        return null;
227
    }
228
229
    /**
230
     * @param string $message
231
     * @param bool $print
232
     * @return void
233
     */
234
    protected function printError($message, $print = true)
235
    {
236
        if ($print) {
237
            $this->stdout($message, Console::FG_RED);
238
        }
239
    }
240
241
    /**
242
     * Generate fields for views template
243
     *
244
     * @return object
245
     */
246
    protected function generateFields()
247
    {
248
        $modelClass     = $this->model::class;
249
        $modelNamespace = str_replace('/', '\\', StringHelper::dirname($modelClass));
250
251
        $schema      = $this->model->tableSchema;
252
        $columns     = $schema->columns;
253
        $foreignKeys = $schema->foreignKeys;
254
        $fields      = [];
255
256
        foreach ($foreignKeys as $fk_str => $foreignKey) {
257
            unset($foreignKeys[$fk_str]);
258
            $table  = array_shift($foreignKey);
259
            $column = array_keys($foreignKey)[0];
260
261
            $model = $this->getClass(($class = $modelNamespace . '\\' . Inflector::camelize($table)));
262
            $foreignKeys[$column] = $model;
263
264
            $this->printError("Class {$class} not exists. Foreign Key for '$column' column will be ignored and a common column will be generated.\n", $model === null);
265
        }
266
267
        foreach ($columns as $column => $data) {
268
            /** @var ColumnSchema $data */
269
            if ($data->autoIncrement) {
270
                continue;
271
            }
272
273
            $foreign = $ref_table_id = null;
274
            if (isset($foreignKeys[$column])) {
275
                $foreign      = $foreignKeys[$column];
276
                $ref_table_id = $foreign->tableSchema->primaryKey[0];
277
            }
278
279
            $faker = $this->generateFakerField($data->name) ?? $this->generateFakerField($data->type);
280
            if ($data->dbType === 'tinyint(1)') {
281
                $faker = 'boolean';
282
            }
283
284
            $fields[$column] = (object) [
285
                'faker'        => $faker,
286
                'foreign'      => $foreign,
287
                'ref_table_id' => $ref_table_id
288
            ];
289
        }
290
291
        return (object) $fields;
292
    }
293
294
    /**
295
     * Generate Faker Field Name
296
     *
297
     * @param string $key
298
     * @return string
299
     */
300
    protected function generateFakerField($key)
301
    {
302
        $faker = [
303
            'full_name'     => 'name',
304
            'name'          => 'name',
305
            'short_name'    => 'firstName',
306
            'first_name'    => 'firstName',
307
            'nickname'      => 'firstName',
308
            'last_name'     => 'lastName',
309
            'description'   => 'realText()',
310
            'company'       => 'company',
311
            'business_name' => 'company',
312
            'email'         => 'email',
313
            'phone'         => 'phoneNumber',
314
            'hp'            => 'phoneNumber',
315
            'start_date'    => 'dateTime()->format("Y-m-d H:i:s")',
316
            'end_date'      => 'dateTime()->format("Y-m-d H:i:s")',
317
            'created_at'    => 'dateTime()->format("Y-m-d H:i:s")',
318
            'updated_at'    => 'dateTime()->format("Y-m-d H:i:s")',
319
            'token'         => 'uuid',
320
            'duration'      => 'numberBetween()',
321
322
            'integer'       => 'numberBetween(0, 10)',
323
            'smallint'      => 'numberBetween(0, 10)',
324
            'tinyint'       => 'numberBetween(0, 10)',
325
            'mediumint'     => 'numberBetween(0, 10)',
326
            'int'           => 'numberBetween(0, 10)',
327
            'bigint'        => 'numberBetween()',
328
            'date'          => 'date()',
329
            'datetime'      => 'dateTime()->format("Y-m-d H:i:s")',
330
            'timestamp'     => 'dateTime()->format("Y-m-d H:i:s")',
331
            'year'          => 'year()',
332
            'time'          => 'time()',
333
        ];
334
335
        return ArrayHelper::getValue($faker, $key, 'word');
336
    }
337
338
    /**
339
     * Get Default Seeder Class if no class selected
340
     *
341
     * @return TableSeeder|null
342
     */
343
    protected function getDefaultSeeder()
344
    {
345
        $defaultSeederClass = "{$this->seederNamespace}\\{$this->defaultSeederClass}";
346
        $defaultSeederFile = "{$defaultSeederClass}.php";
347
348
        if (!class_exists($defaultSeederClass) || !file_exists($defaultSeederFile)) {
349
            FileHelper::createDirectory($this->seederPath);
350
            $content = $this->renderFile($this->templateSeederFile, [
351
                'namespace' => $this->seederNamespace,
352
            ]);
353
354
            $this->stdout("\nClass {$defaultSeederClass} created in {$defaultSeederFile}.\n");
355
356
            file_put_contents($defaultSeederFile, $content, LOCK_EX);
357
        }
358
359
        if (($seederClass = new $defaultSeederClass) instanceof TableSeeder) {
360
            /** @var TableSeeder $seederClass */
361
            return $seederClass;
362
        }
363
364
        return null;
365
    }
366
367
    /**
368
     * Execute function
369
     *
370
     * @param array|string $class
371
     * @param string $method
372
     * @param string|null $defaultClass
373
     * @return void
374
     */
375
    protected function runMethod($class, $method = 'run', $defaultClass = null)
376
    {
377
        $count = 0;
378
        foreach ((array) $class as $seederClass) {
379
            if ($seeder = $this->getClass($seederClass)) {
380
                $seeder->{$method}();
381
                $count++;
382
                break;
383
            }
384
        }
385
        $defaultClass = $defaultClass ?? $class[0];
386
        $this->printError("Class {$defaultClass} not exists.\n", $count === 0);
387
    }
388
389
    /**
390
     * @param string $modelNamespace
391
     * @param string $modelName
392
     * @return string
393
     */
394
    private function normalizeFile($modelNamespace, $modelName)
395
    {
396
        $file = "{$modelNamespace}\\{$modelName}";
397
        if (strpos($modelName, '\\') !== false) {
398
            $explode         = explode('\\', $modelName);
399
            $modelName       = array_pop($explode);
400
            $modelNamespace .= '\\' . implode('\\', $explode);
401
402
            $file = "{$modelNamespace}\\{$modelName}";
403
        }
404
        if (!class_exists($file)) {
405
            $modelName = Inflector::camelize($modelName);
406
            $file      = "{$modelNamespace}\\{$modelName}";
407
        }
408
409
        return $file;
410
    }
411
}
412