Passed
Push — main ( 3938b8...14b83a )
by Sugeng
02:21
created

SeederController::printError()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 2
nc 2
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
            $func = $function ?? 'run';
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
            $count = 0;
113
            foreach ($seederClasses as $seederClass) {
114
                if ($seeder = $this->getClass($seederClass)) {
115
                    $seeder->{$func}();
116
                    $count++;
117
                    break;
118
                }
119
            }
120
            $this->printError("Class {$name} not exists.\n", $count === 0);
121
        } else if (($defaultSeeder = $this->getDefaultSeeder()) !== null) {
122
            $defaultSeeder->run();
123
        }
124
125
        return ExitCode::OK;
126
    }
127
128
    /**
129
     * Create a new seeder.
130
     *
131
     * This command create a new seeder using the available seeder template.
132
     * After using this command, developers should modify the created seeder
133
     * skeleton by filling up the actual seeder logic.
134
     *
135
     * ```shell
136
     * yii seeder/create model_name
137
     * ```
138
     * or
139
     * ```shell
140
     * yii seeder/create modelName
141
     * ```
142
     * or
143
     * ```shell
144
     * yii seeder/create model-name
145
     * ```
146
     * 
147
     * @see https://www.yiiframework.com/doc/api/2.0/yii-helpers-inflector#camelize()-detail
148
     *
149
     * For example:
150
     *
151
     * ```shell
152
     * yii seeder/create user
153
     * ```
154
     * or
155
     * ```shell
156
     * yii seeder/create example/user
157
     * ```
158
     * if User's Model directory is "common\models\example\User", this default use `$modelNamespace` configuration
159
     * 
160
     * or you can use full path of your class name
161
     * 
162
     * ```shell
163
     * yii seeder/create \app\models\User
164
     * ```
165
     * or
166
     * ```shell
167
     * yii seeder/create \backend\modules\example\models\User
168
     * ```
169
     *
170
     * @param string $modelName the name of the new seeder or class
171
     *
172
     * @return int ExitCode::OK
173
     */
174
    public function actionCreate($modelName)
175
    {
176
        $modelName = str_replace('/', '\\', $modelName);
177
178
        $this->model = $this->getClass($modelName);
179
        if ($this->model === null) {
180
            $modelNamespace = $this->modelNamespace;
181
            $file           = "{$modelNamespace}\\{$modelName}";
182
            if (strpos($modelName, '\\')) {
183
                $explode         = explode('\\', $modelName);
184
                $modelName       = array_pop($explode);
185
                $modelNamespace .= '\\' . implode('\\', $explode);
186
187
                $file = "{$modelNamespace}\\{$modelName}";
188
            }
189
            if (!class_exists($file)) {
190
                $modelName = Inflector::camelize($modelName);
191
                $file      = "{$modelNamespace}\\{$modelName}";
192
            }
193
194
            $this->model = $this->getClass($file);
195
            if ($this->model === null) {
196
                $this->printError("Class {$file} not exists.\n");
197
198
                return ExitCode::OK;
199
            }
200
        }
201
202
        $modelClass = $this->model::class;
203
        $className  = StringHelper::basename($modelClass) . 'TableSeeder';
204
        $file       = "{$this->tablesPath}/{$className}.php";
205
        if ($this->confirm("Create new seeder '{$file}'?")) {
206
            $content = $this->renderFile($this->templateTableFile, [
207
                'className' => $className,
208
                'namespace' => $this->tableSeederNamespace,
209
                'model'     => $this->model,
210
                'fields'    => $this->generateFields(),
211
            ]);
212
            FileHelper::createDirectory($this->tablesPath);
213
214
            if (!file_exists($file) || $this->confirm("\n'{$className}' already exists, overwrite?\nAll data will be lost irreversibly!")) {
215
                file_put_contents($file, $content, LOCK_EX);
216
                $this->stdout("New seeder created successfully.\n", Console::FG_GREEN);
217
            }
218
        }
219
220
        return ExitCode::OK;
221
    }
222
223
    /**
224
     * @param string $path
225
     * @return \yii\db\ActiveRecord|null
226
     */
227
    protected function getClass($path)
228
    {
229
        if (class_exists($path)) {
230
            return new $path;
231
        }
232
233
        return null;
234
    }
235
236
    /**
237
     * @param string $message
238
     * @param bool $print
239
     * @return void
240
     */
241
    protected function printError($message, $print = true)
242
    {
243
        if ($print) {
244
            $this->stdout($message, Console::FG_RED);
245
        }
246
    }
247
248
    /**
249
     * Generate fields for views template
250
     *
251
     * @return object
252
     */
253
    protected function generateFields()
254
    {
255
        $modelClass     = $this->model::class;
256
        $modelNamespace = str_replace('/', '\\', StringHelper::dirname($modelClass));
257
258
        $schema      = $this->model->tableSchema;
259
        $columns     = $schema->columns;
260
        $foreignKeys = $schema->foreignKeys;
261
        $fields      = [];
262
263
        foreach ($foreignKeys as $fk_str => $foreignKey) {
264
            unset($foreignKeys[$fk_str]);
265
            $table  = array_shift($foreignKey);
266
            $column = array_keys($foreignKey)[0];
267
268
            $model = $this->getClass(($class = $modelNamespace . '\\' . Inflector::camelize($table)));
269
            $foreignKeys[$column] = $model;
270
271
            $this->printError("Class {$class} not exists. Foreign Key for '$column' column will be ignored and a common column will be generated.\n", $model === null);
272
        }
273
274
        foreach ($columns as $column => $data) {
275
            /** @var ColumnSchema $data */
276
            if ($data->autoIncrement) {
277
                continue;
278
            }
279
280
            $foreign = $ref_table_id = null;
281
            if (isset($foreignKeys[$column])) {
282
                $foreign      = $foreignKeys[$column];
283
                $ref_table_id = $foreign->tableSchema->primaryKey[0];
284
            }
285
286
            $faker = $this->generateFakerField($data->name) ?? $this->generateFakerField($data->type);
287
            if ($data->dbType === 'tinyint(1)') {
288
                $faker = 'boolean';
289
            }
290
291
            $fields[$column] = (object) [
292
                'faker'        => $faker,
293
                'foreign'      => $foreign,
294
                'ref_table_id' => $ref_table_id
295
            ];
296
        }
297
298
        return (object) $fields;
299
    }
300
301
    /**
302
     * Generate Faker Field Name
303
     *
304
     * @param string $key
305
     * @return string
306
     */
307
    protected function generateFakerField($key)
308
    {
309
        $faker = [
310
            'full_name'     => 'name',
311
            'name'          => 'name',
312
            'short_name'    => 'firstName',
313
            'first_name'    => 'firstName',
314
            'nickname'      => 'firstName',
315
            'last_name'     => 'lastName',
316
            'description'   => 'realText()',
317
            'company'       => 'company',
318
            'business_name' => 'company',
319
            'email'         => 'email',
320
            'phone'         => 'phoneNumber',
321
            'hp'            => 'phoneNumber',
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(0, 10)',
328
            'date'          => 'date()',
329
            'datetime'      => 'dateTime()',
330
            'timestamp'     => 'dateTime()',
331
            'year'          => 'year()',
332
            'time'          => 'time()',
333
        ];
334
335
        return ArrayHelper::getValue($faker, $key, 'text');
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