Passed
Push — main ( 286066...4e558b )
by Sugeng
04:55 queued 02:33
created

SeederController::runMethod()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 12
rs 10
cc 3
nc 3
nop 3
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           = "{$modelNamespace}\\{$modelName}";
186
            if (strpos($modelName, '\\')) {
187
                $explode         = explode('\\', $modelName);
188
                $modelName       = array_pop($explode);
189
                $modelNamespace .= '\\' . implode('\\', $explode);
190
191
                $file = "{$modelNamespace}\\{$modelName}";
192
            }
193
            if (!class_exists($file)) {
194
                $modelName = Inflector::camelize($modelName);
195
                $file      = "{$modelNamespace}\\{$modelName}";
196
            }
197
198
            $this->model = $this->getClass($file);
199
            if ($this->model === null) {
200
                $this->printError("Class {$file} not exists.\n");
201
202
                return ExitCode::OK;
203
            }
204
        }
205
206
        $modelClass = $this->model::class;
207
        $className  = StringHelper::basename($modelClass) . 'TableSeeder';
208
        $file       = "{$this->tablesPath}/{$className}.php";
209
        if ($this->confirm("Create new seeder '{$file}'?")) {
210
            $content = $this->renderFile($this->templateTableFile, [
211
                'className' => $className,
212
                'namespace' => $this->tableSeederNamespace,
213
                'model'     => $this->model,
214
                'fields'    => $this->generateFields(),
215
            ]);
216
            FileHelper::createDirectory($this->tablesPath);
217
218
            if (!file_exists($file) || $this->confirm("\n'{$file}' already exists, overwrite?\nAll data will be lost irreversibly!")) {
219
                file_put_contents($file, $content, LOCK_EX);
220
                $this->stdout("New seeder created successfully.\n", Console::FG_GREEN);
221
            }
222
        }
223
224
        return ExitCode::OK;
225
    }
226
227
    /**
228
     * @param string $path
229
     * @return \yii\db\ActiveRecord|null
230
     */
231
    protected function getClass($path)
232
    {
233
        if (class_exists($path)) {
234
            return new $path;
235
        }
236
237
        return null;
238
    }
239
240
    /**
241
     * @param string $message
242
     * @param bool $print
243
     * @return void
244
     */
245
    protected function printError($message, $print = true)
246
    {
247
        if ($print) {
248
            $this->stdout($message, Console::FG_RED);
249
        }
250
    }
251
252
    /**
253
     * Generate fields for views template
254
     *
255
     * @return object
256
     */
257
    protected function generateFields()
258
    {
259
        $modelClass     = $this->model::class;
260
        $modelNamespace = str_replace('/', '\\', StringHelper::dirname($modelClass));
261
262
        $schema      = $this->model->tableSchema;
263
        $columns     = $schema->columns;
264
        $foreignKeys = $schema->foreignKeys;
265
        $fields      = [];
266
267
        foreach ($foreignKeys as $fk_str => $foreignKey) {
268
            unset($foreignKeys[$fk_str]);
269
            $table  = array_shift($foreignKey);
270
            $column = array_keys($foreignKey)[0];
271
272
            $model = $this->getClass(($class = $modelNamespace . '\\' . Inflector::camelize($table)));
273
            $foreignKeys[$column] = $model;
274
275
            $this->printError("Class {$class} not exists. Foreign Key for '$column' column will be ignored and a common column will be generated.\n", $model === null);
276
        }
277
278
        foreach ($columns as $column => $data) {
279
            /** @var ColumnSchema $data */
280
            if ($data->autoIncrement) {
281
                continue;
282
            }
283
284
            $foreign = $ref_table_id = null;
285
            if (isset($foreignKeys[$column])) {
286
                $foreign      = $foreignKeys[$column];
287
                $ref_table_id = $foreign->tableSchema->primaryKey[0];
288
            }
289
290
            $faker = $this->generateFakerField($data->name) ?? $this->generateFakerField($data->type);
291
            if ($data->dbType === 'tinyint(1)') {
292
                $faker = 'boolean';
293
            }
294
295
            $fields[$column] = (object) [
296
                'faker'        => $faker,
297
                'foreign'      => $foreign,
298
                'ref_table_id' => $ref_table_id
299
            ];
300
        }
301
302
        return (object) $fields;
303
    }
304
305
    /**
306
     * Generate Faker Field Name
307
     *
308
     * @param string $key
309
     * @return string
310
     */
311
    protected function generateFakerField($key)
312
    {
313
        $faker = [
314
            'full_name'     => 'name',
315
            'name'          => 'name',
316
            'short_name'    => 'firstName',
317
            'first_name'    => 'firstName',
318
            'nickname'      => 'firstName',
319
            'last_name'     => 'lastName',
320
            'description'   => 'realText()',
321
            'company'       => 'company',
322
            'business_name' => 'company',
323
            'email'         => 'email',
324
            'phone'         => 'phoneNumber',
325
            'hp'            => 'phoneNumber',
326
            'start_date'    => 'dateTime()->format("Y-m-d H:i:s")',
327
            'end_date'      => 'dateTime()->format("Y-m-d H:i:s")',
328
            'created_at'    => 'dateTime()->format("Y-m-d H:i:s")',
329
            'updated_at'    => 'dateTime()->format("Y-m-d H:i:s")',
330
            'token'         => 'uuid',
331
            'duration'      => 'numberBetween()',
332
333
            'integer'       => 'numberBetween(0, 10)',
334
            'smallint'      => 'numberBetween(0, 10)',
335
            'tinyint'       => 'numberBetween(0, 10)',
336
            'mediumint'     => 'numberBetween(0, 10)',
337
            'int'           => 'numberBetween(0, 10)',
338
            'bigint'        => 'numberBetween()',
339
            'date'          => 'date()',
340
            'datetime'      => 'dateTime()->format("Y-m-d H:i:s")',
341
            'timestamp'     => 'dateTime()->format("Y-m-d H:i:s")',
342
            'year'          => 'year()',
343
            'time'          => 'time()',
344
        ];
345
346
        return ArrayHelper::getValue($faker, $key, 'word');
347
    }
348
349
    /**
350
     * Get Default Seeder Class if no class selected
351
     *
352
     * @return TableSeeder|null
353
     */
354
    protected function getDefaultSeeder()
355
    {
356
        $defaultSeederClass = "{$this->seederNamespace}\\{$this->defaultSeederClass}";
357
        $defaultSeederFile = "{$defaultSeederClass}.php";
358
359
        if (!class_exists($defaultSeederClass) || !file_exists($defaultSeederFile)) {
360
            FileHelper::createDirectory($this->seederPath);
361
            $content = $this->renderFile($this->templateSeederFile, [
362
                'namespace' => $this->seederNamespace,
363
            ]);
364
365
            $this->stdout("\nClass {$defaultSeederClass} created in {$defaultSeederFile}.\n");
366
367
            file_put_contents($defaultSeederFile, $content, LOCK_EX);
368
        }
369
370
        if (($seederClass = new $defaultSeederClass) instanceof TableSeeder) {
371
            /** @var TableSeeder $seederClass */
372
            return $seederClass;
373
        }
374
375
        return null;
376
    }
377
378
    /**
379
     * Execute function
380
     *
381
     * @param array|string $class
382
     * @param string $method
383
     * @param string|null $defaultClass
384
     * @return void
385
     */
386
    protected function runMethod($class, $method = 'run', $defaultClass = null)
387
    {
388
        $count = 0;
389
        foreach ((array) $class as $seederClass) {
390
            if ($seeder = $this->getClass($seederClass)) {
391
                $seeder->{$method}();
392
                $count++;
393
                break;
394
            }
395
        }
396
        $defaultClass = $defaultClass ?? $class[0];
0 ignored issues
show
Unused Code introduced by
The assignment to $defaultClass is dead and can be removed.
Loading history...
397
        $this->printError("Class {$class[0]} not exists.\n", $count === 0);
398
    }
399
}
400