Passed
Push — main ( b1864e...85633e )
by Sugeng
02:19
created

SeederController::generateFields()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 47
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 28
c 2
b 0
f 0
dl 0
loc 47
rs 8.8497
cc 6
nc 12
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\helpers\Console;
10
use yii\helpers\FileHelper;
11
use yii\helpers\Inflector;
12
use yii\helpers\StringHelper;
13
14
/**
15
 * Class SeederController
16
 * 
17
 * @package diecoding\seeder
18
 * 
19
 * @link [sugeng-sulistiyawan.github.io](sugeng-sulistiyawan.github.io)
20
 * @author Sugeng Sulistiyawan <[email protected]>
21
 * @copyright Copyright (c) 2023
22
 */
23
class SeederController extends Controller
24
{
25
    /** @var string the default command action. */
26
    public $defaultAction = 'seed';
27
28
    /** @var string seeder path, support path alias */
29
    public $seederPath = '@console/seeder';
30
31
    /** @var string seeder namespace */
32
    public $seederNamespace = 'console\seeder';
33
34
    /** 
35
     * @var string this class look like `$this->seederNamespace\Seeder` 
36
     * default seeder class run if no class selected, 
37
     * must instance of `\diecoding\seeder\TableSeeder` 
38
     */
39
    public $defaultSeederClass = 'Seeder';
40
41
    /** @var string tables path, support path alias */
42
    public $tablesPath = '@console/seeder/tables';
43
44
    /** @var string seeder table namespace */
45
    public $tableSeederNamespace = 'console\seeder\tables';
46
47
    /** @var string model namespace */
48
    public $modelNamespace = 'common\models';
49
50
    /** @var string path view template table seeder, support path alias */
51
    public $templateSeederFile = '@diecoding/seeder/views/Seeder.php';
52
53
    /** @var string path view template seeder, support path alias */
54
    public $templateTableFile = '@diecoding/seeder/views/TableSeeder.php';
55
56
    /** @var bool run on production or Seeder on YII_ENV === 'prod' */
57
    public $runOnProd;
58
59
    /** @var \yii\db\ActiveRecord */
60
    protected $model = null;
61
62
    /**
63
     * {@inheritdoc}
64
     */
65
    public function options($actionID)
66
    {
67
        return ['runOnProd'];
68
    }
69
70
    /**
71
     * @inheritdoc
72
     */
73
    public function init()
74
    {
75
        parent::init();
76
77
        $this->seederPath         = (string) Yii::getAlias($this->seederPath);
78
        $this->tablesPath         = (string) Yii::getAlias($this->tablesPath);
79
        $this->templateSeederFile = (string) Yii::getAlias($this->templateSeederFile);
80
        $this->templateTableFile  = (string) Yii::getAlias($this->templateTableFile);
81
    }
82
83
    /**
84
     * Seed action
85
     *
86
     * @param string $name
87
     * @return int ExitCode::OK
88
     */
89
    public function actionSeed($name = "")
90
    {
91
        if (YII_ENV_PROD && !$this->runOnProd) {
92
            $this->stdout("YII_ENV is set to 'prod'.\nUse seeder is not possible on production systems. use '--runOnProd' to ignore it.\n");
93
            return ExitCode::OK;
94
        }
95
96
        $explode  = explode(':', $name);
97
        $name     = $explode[0];
98
        $function = $explode[1] ?? null;
99
100
        if ($name) {
101
            $func = $function ?? 'run';
102
103
            $seederClasses = [
104
                $name,
105
                "{$name}TableSeeder",
106
                "{$this->seederNamespace}\\{$name}",
107
                "{$this->seederNamespace}\\{$name}TableSeeder",
108
                "{$this->tableSeederNamespace}\\{$name}",
109
                "{$this->tableSeederNamespace}\\{$name}TableSeeder",
110
            ];
111
112
            foreach ($seederClasses as $seederClass) {
113
                if ($seeder = $this->getClass($seederClass)) {
114
                    $seeder->{$func}();
115
                    break;
116
                }
117
            }
118
        } else if (($defaultSeeder = $this->getDefaultSeeder()) !== null) {
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $defaultSeeder is correct as $this->getDefaultSeeder() targeting diecoding\seeder\SeederC...ler::getDefaultSeeder() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
introduced by
The condition $defaultSeeder = $this->...efaultSeeder() !== null is always false.
Loading history...
119
            $defaultSeeder->run();
120
        }
121
122
        return ExitCode::OK;
123
    }
124
125
    /**
126
     * Create a new seeder.
127
     *
128
     * This command create a new seeder using the available seeder template.
129
     * After using this command, developers should modify the created seeder
130
     * skeleton by filling up the actual seeder logic.
131
     *
132
     * ```shell
133
     * yii seeder/create model_name
134
     * ```
135
     * or
136
     * ```shell
137
     * yii seeder/create modelName
138
     * ```
139
     * or
140
     * ```shell
141
     * yii seeder/create model-name
142
     * ```
143
     * 
144
     * @see https://www.yiiframework.com/doc/api/2.0/yii-helpers-inflector#camelize()-detail
145
     *
146
     * For example:
147
     *
148
     * ```shell
149
     * yii seeder/create user
150
     * ```
151
     * or
152
     * ```shell
153
     * yii seeder/create example/user
154
     * ```
155
     * if User's Model directory is "common\models\example\User", this default use `$modelNamespace` configuration
156
     * 
157
     * or you can use full path of your class name
158
     * 
159
     * ```shell
160
     * yii seeder/create \app\models\User
161
     * ```
162
     * or
163
     * ```shell
164
     * yii seeder/create \backend\modules\example\models\User
165
     * ```
166
     *
167
     * @param string $modelName the name of the new seeder or class
168
     *
169
     * @return int ExitCode::OK
170
     */
171
    public function actionCreate($modelName)
172
    {
173
        $modelName = str_replace('/', '\\', $modelName);
174
175
        if (class_exists($modelName)) {
176
            $this->model = $this->getClass($modelName);
177
        } else {
178
            $modelNamespace = $this->modelNamespace;
179
180
            if (strpos($modelName, '\\')) {
181
                $explode         = explode('\\', $modelName);
182
                $modelName       = array_pop($explode);
183
                $modelNamespace .= '\\' . implode('\\', $explode);
184
185
                $file = "{$modelNamespace}\\{$modelName}";
186
                if (!class_exists($file)) {
187
                    $modelName = Inflector::camelize($modelName);
188
                    $file      = "{$modelNamespace}\\{$modelName}";
189
                }
190
            } else {
191
                $file = "{$modelNamespace}\\{$modelName}";
192
                if (!class_exists($file)) {
193
                    $modelName = Inflector::camelize($modelName);
194
                    $file      = "{$modelNamespace}\\{$modelName}";
195
                }
196
            }
197
198
            $this->model = $this->getClass($file);
199
        }
200
201
        if ($this->model === null) {
202
            return ExitCode::OK;
203
        }
204
205
        $modelClass = $this->model::class;
206
        $className  = StringHelper::basename($modelClass) . 'TableSeeder';
207
        $file       = "{$this->tablesPath}/{$className}.php";
208
        if ($this->confirm("Create new seeder '{$file}'?")) {
209
            $content = $this->renderFile($this->templateTableFile, [
210
                'className' => $className,
211
                'namespace' => $this->tableSeederNamespace,
212
                'model'     => $this->model,
213
                'fields'    => $this->generateFields(),
214
            ]);
215
            FileHelper::createDirectory($this->tablesPath);
216
217
            if (!file_exists($file) || $this->confirm("\n'{$className}' already exists, overwrite?\nAll data will be lost irreversibly!")) {
218
                file_put_contents($file, $content, LOCK_EX);
219
                $this->stdout("New seeder created successfully.\n", Console::FG_GREEN);
220
            }
221
        }
222
223
        return ExitCode::OK;
224
    }
225
226
    /**
227
     * @param string $path
228
     * @param string $eol
229
     * @return \yii\db\ActiveRecord|null
230
     */
231
    protected function getClass($path, $eol = PHP_EOL)
232
    {
233
        if (class_exists($path)) {
234
            return new $path;
235
        }
236
237
        $this->stdout("Class {$path} not exists. {$eol}");
238
        return null;
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
            $errorMsg = "Foreign Key for '$column' column will be ignored and a common column will be generated.\n";
262
263
            $model = $this->getClass($modelNamespace . '\\' . Inflector::camelize($table), $errorMsg);
264
            $foreignKeys[$column] = $model;
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 = $faker = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $faker is dead and can be removed.
Loading history...
274
275
            if (isset($foreignKeys[$column])) {
276
                $foreign      = $foreignKeys[$column];
277
                $ref_table_id = $foreign->tableSchema->primaryKey[0];
278
            }
279
280
            $faker = $this->generateFakerName($data);
281
            if (empty($faker)) {
282
                $faker = $this->generateFakerType($data);
283
            }
284
285
            $fields[$column] = (object) [
286
                'faker'        => $faker,
287
                'foreign'      => $foreign,
288
                'ref_table_id' => $ref_table_id
289
            ];
290
        }
291
292
        return (object) $fields;
293
    }
294
295
    /**
296
     * Generate Faker Field Name
297
     *
298
     * @param ColumnSchema $data
299
     * @return string
300
     */
301
    protected function generateFakerName(ColumnSchema $data)
302
    {
303
        switch ($data->name) {
304
            case 'full_name':
305
            case 'name':
306
                $faker = 'name';
307
                break;
308
            case 'short_name':
309
            case 'first_name':
310
            case 'nickname':
311
                $faker = 'firstName';
312
                break;
313
            case 'last_name':
314
                $faker = 'lastName';
315
                break;
316
            case 'description':
317
                $faker = 'realText()';
318
                break;
319
            case 'company':
320
            case 'business_name':
321
                $faker = 'company';
322
                break;
323
            case 'email':
324
                $faker = 'email';
325
                break;
326
            case 'phone':
327
            case 'hp':
328
                $faker = 'phoneNumber';
329
                break;
330
        }
331
332
        return $faker;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $faker does not seem to be defined for all execution paths leading up to this point.
Loading history...
333
    }
334
335
    /**
336
     * Generate Faker Field Type
337
     *
338
     * @param ColumnSchema $data
339
     * @return string
340
     */
341
    protected function generateFakerType(ColumnSchema $data)
342
    {
343
        switch ($data->type) {
344
            case 'integer':
345
            case 'smallint':
346
            case 'tinyint':
347
                $faker = 'numberBetween(0, 10)';
348
                if ($data->dbType === 'tinyint(1)') {
349
                    $faker = 'boolean';
350
                    break;
351
                }
352
            case 'mediumint':
353
            case 'int':
354
            case 'bigint':
355
                $faker = 'numberBetween(0, 10)';
356
                break;
357
            case 'date':
358
                $faker = 'date()';
359
                break;
360
            case 'datetime':
361
            case 'timestamp':
362
                $faker = 'dateTime()';
363
                break;
364
            case 'year':
365
                $faker = 'year()';
366
                break;
367
            case 'time':
368
                $faker = 'time()';
369
                break;
370
            default:
371
                $faker = 'text';
372
        }
373
374
        return $faker;
375
    }
376
377
    /**
378
     * Get Default Seeder Class if no class selected
379
     *
380
     * @return TableSeeder|null
381
     */
382
    protected function getDefaultSeeder()
383
    {
384
        $defaultSeederClass = "{$this->seederNamespace}\\{$this->defaultSeederClass}";
385
        $defaultSeederFile = "{$defaultSeederClass}.php";
386
387
        if (!class_exists($defaultSeederClass) || !file_exists($defaultSeederFile)) {
388
            FileHelper::createDirectory($this->seederPath);
389
            $content = $this->renderFile($this->templateSeederFile, [
390
                'namespace' => $this->seederNamespace,
391
            ]);
392
393
            $this->stdout("\nClass {$defaultSeederClass} created in {$defaultSeederFile}.\n");
394
395
            file_put_contents($defaultSeederFile, $content, LOCK_EX);
396
        }
397
398
        if ($defaultSeederClass instanceof TableSeeder) {
0 ignored issues
show
introduced by
$defaultSeederClass is never a sub-type of diecoding\seeder\TableSeeder.
Loading history...
399
            /** @var TableSeeder $defaultSeederClass */
400
            return new $defaultSeederClass;
401
        }
402
403
        $this->stdout("\nClass {$defaultSeederClass} must instance of `\diecoding\seeder\TableSeeder`.\n");
404
        return null;
405
    }
406
}
407