1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* _ __ __ _____ _____ ___ ____ _____ |
5
|
|
|
* | | / // // ___//_ _// || __||_ _| |
6
|
|
|
* | |/ // /(__ ) / / / /| || | | | |
7
|
|
|
* |___//_//____/ /_/ /_/ |_||_| |_| |
8
|
|
|
* @link http://vistart.name/ |
9
|
|
|
* @copyright Copyright (c) 2015 vistart |
10
|
|
|
* @license http://vistart.name/license/ |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
namespace vistart\Models\gii\baseUserModel; |
14
|
|
|
|
15
|
|
|
use Yii; |
16
|
|
|
use yii\db\ActiveQuery; |
17
|
|
|
use yii\db\ActiveRecord; |
18
|
|
|
use yii\db\Connection; |
19
|
|
|
use yii\db\Schema; |
20
|
|
|
use yii\gii\CodeFile; |
21
|
|
|
use yii\helpers\Inflector; |
22
|
|
|
use yii\base\NotSupportedException; |
23
|
|
|
use vistart\Models\models\BaseUserModel; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Description of Generator |
27
|
|
|
* |
28
|
|
|
* @author vistart <[email protected]> |
29
|
|
|
*/ |
30
|
|
|
class Generator extends \yii\gii\Generator { |
31
|
|
|
|
32
|
|
|
public $db = 'db'; |
33
|
|
|
public $guidAttribute = 'guid'; |
34
|
|
|
public $idAttribute = 'id'; |
35
|
|
|
public $idAttributeType = ''; |
36
|
|
|
public $idAttributeTypes = [ |
37
|
|
|
0 => 'Random Number', |
38
|
|
|
1 => 'Random String', |
39
|
|
|
2 => 'Auto Increment', |
40
|
|
|
]; |
41
|
|
|
public $passwordHashAttribute = 'pass_hash'; |
42
|
|
|
public $createdAtAttribute = 'create_time'; |
43
|
|
|
public $updatedAtAttribute = 'update_time'; |
44
|
|
|
public $enableIP = true; |
45
|
|
|
public $ipAttribute1 = 'ip_1'; |
46
|
|
|
public $ipAttribute2 = 'ip_2'; |
47
|
|
|
public $ipAttribute3 = 'ip_3'; |
48
|
|
|
public $ipAttribute4 = 'ip_4'; |
49
|
|
|
public $ipTypeAttribute = 'ip_type'; |
50
|
|
|
public $authKeyAttribute = 'auth_key'; |
51
|
|
|
public $accessTokenAttribute = 'access_token'; |
52
|
|
|
public $passwordResetTokenAttribute = 'password_reset_token'; |
53
|
|
|
public $statusAttribute = 'status'; |
54
|
|
|
public $sourceAttribute = 'source'; |
55
|
|
|
public $ns = 'app\models'; |
56
|
|
|
public $tableName; |
57
|
|
|
public $modelClass; |
58
|
|
|
public $baseClass = 'vistart\Models\models\BaseUserModel'; |
59
|
|
|
public $generatePresettingRules = false; |
60
|
|
|
public $generatePresettingBehaviors = false; |
61
|
|
|
public $generateRelations = true; |
62
|
|
|
public $generateLabelsFromComments = false; |
63
|
|
|
public $useTablePrefix = false; |
64
|
|
|
public $useSchemaName = true; |
65
|
|
|
public $generateQuery = false; |
66
|
|
|
public $queryNs = 'app\models'; |
67
|
|
|
public $queryClass; |
68
|
|
|
public $queryBaseClass = 'yii\db\ActiveQuery'; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* @inheritdoc |
72
|
|
|
*/ |
73
|
|
|
public function getName() { |
74
|
|
|
return 'User Model Generator'; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* @inheritdoc |
79
|
|
|
*/ |
80
|
|
|
public function getDescription() { |
81
|
|
|
return 'This generator generates an BaseUserModel class for the specified database table.'; |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* @inheritdoc |
86
|
|
|
*/ |
87
|
|
|
public function rules() { |
88
|
|
|
return array_merge(parent::rules(), [ |
89
|
|
|
[['db', 'ns', 'tableName', 'guidAttribute', 'idAttribute', 'passwordHashAttribute', 'createdAtAttribute', 'updatedAtAttribute', 'ipAttribute1', 'ipAttribute2', 'ipAttribute3', 'ipAttribute4', 'ipTypeAttribute', 'authKeyAttribute', 'accessTokenAttribute', 'passwordResetTokenAttribute', 'statusAttribute', 'sourceAttribute', 'modelClass', 'baseClass', 'queryNs', 'queryClass', 'queryBaseClass'], 'filter', 'filter' => 'trim'], |
90
|
|
|
[['ns', 'queryNs'], 'filter', 'filter' => function($value) { |
91
|
|
|
return trim($value, '\\'); |
92
|
|
|
}], |
93
|
|
|
[['db', 'ns', 'tableName', 'baseClass', 'queryNs', 'queryBaseClass', 'guidAttribute', 'passwordHashAttribute', 'authKeyAttribute', 'accessTokenAttribute', 'passwordResetTokenAttribute'], 'required'], |
94
|
|
|
[['db', 'modelClass', 'queryClass'], 'match', 'pattern' => '/^\w+$/', 'message' => 'Only word characters are allowed.'], |
95
|
|
|
[['ns', 'baseClass', 'queryNs', 'queryBaseClass'], 'match', 'pattern' => '/^[\w\\\\]+$/', 'message' => 'Only word characters and backslashes are allowed.'], |
96
|
|
|
[['tableName'], 'match', 'pattern' => '/^(\w+\.)?([\w\*]+)$/', 'message' => 'Only word characters, and optionally an asterisk and/or a dot are allowed.'], |
97
|
|
|
[['db'], 'validateDb'], |
98
|
|
|
[['ns', 'queryNs'], 'validateNamespace'], |
99
|
|
|
[['tableName'], 'validateTableName'], |
100
|
|
|
[['modelClass'], 'validateModelClass', 'skipOnEmpty' => false], |
101
|
|
|
[['baseClass'], 'validateClass', 'params' => ['extends' => ActiveRecord::className()]], |
102
|
|
|
[['queryBaseClass'], 'validateClass', 'params' => ['extends' => ActiveQuery::className()]], |
103
|
|
|
[['enableIP', 'generatePresettingRules', 'generatePresettingBehaviors', 'generateRelations', 'generateLabelsFromComments', 'useTablePrefix', 'useSchemaName', 'generateQuery'], 'boolean'], |
104
|
|
|
[['guidAttribute', 'idAttribute', 'passwordHashAttribute', 'createdAtAttribute', 'updatedAtAttribute', 'ipAttribute1', 'ipAttribute2', 'ipAttribute3', 'ipAttribute4', 'ipTypeAttribute', 'authKeyAttribute', 'accessTokenAttribute', 'passwordResetTokenAttribute', 'statusAttribute', 'sourceAttribute'], 'validateFieldsExist'], |
105
|
|
|
[['enableIP', 'ipAttribute1', 'ipAttribute2', 'ipAttribute3', 'ipAttribute4', 'ipTypeAttribute'], 'validateIP'], |
106
|
|
|
[['enableI18N'], 'boolean'], |
107
|
|
|
[['messageCategory'], 'validateMessageCategory', 'skipOnEmpty' => false], |
108
|
|
|
]); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* @inheritdoc |
113
|
|
|
*/ |
114
|
|
|
public function attributeLabels() { |
115
|
|
|
return array_merge(parent::attributeLabels(), [ |
116
|
|
|
'ns' => 'Namespace', |
117
|
|
|
'db' => 'Database Connection ID', |
118
|
|
|
// 'guidAttribute' => 'guid', |
119
|
|
|
// 'idAttribute' => 'id', |
120
|
|
|
// 'passwordHashAttribute' => 'pass_hash', |
121
|
|
|
// 'createdAtAttribute' => 'create_time', |
122
|
|
|
// 'updatedAtAttribute' => 'update_time', |
123
|
|
|
// 'enableIP' => 'enableIP', |
124
|
|
|
// 'ipAttribute1' => 'ip_1', |
125
|
|
|
// 'ipAttribute2' => 'ip_2', |
126
|
|
|
// 'ipAttribute3' => 'ip_3', |
127
|
|
|
// 'ipAttribute4' => 'ip_4', |
128
|
|
|
// 'ipTypeAttribute' => 'ip_type', |
129
|
|
|
// 'authKeyAttribute' => 'auth_key', |
130
|
|
|
// 'accessTokenAttribute' => 'access_token', |
131
|
|
|
// 'passwordResetTokenAttribute' => 'password_reset_token', |
132
|
|
|
// 'statusAttribute' => 'status', |
133
|
|
|
// 'sourceAttribute' => 'source', |
134
|
|
|
'tableName' => 'Table Name', |
135
|
|
|
'modelClass' => 'Model Class', |
136
|
|
|
'baseClass' => 'Base Class', |
137
|
|
|
'generatePresettingRules' => 'Generate Presetting Rules', |
138
|
|
|
'generatePresettingBehaviors' => 'Generate Presetting Behaviors', |
139
|
|
|
'generateRelations' => 'Generate Relations', |
140
|
|
|
'generateLabelsFromComments' => 'Generate Labels from DB Comments', |
141
|
|
|
'generateQuery' => 'Generate ActiveQuery', |
142
|
|
|
'queryNs' => 'ActiveQuery Namespace', |
143
|
|
|
'queryClass' => 'ActiveQuery Class', |
144
|
|
|
'queryBaseClass' => 'ActiveQuery Base Class', |
145
|
|
|
]); |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* @inheritdoc |
150
|
|
|
*/ |
151
|
|
|
public function hints() { |
152
|
|
|
return array_merge(parent::hints(), [ |
153
|
|
|
'ns' => 'This is the namespace of the ActiveRecord class to be generated, e.g., <code>app\models</code>', |
154
|
|
|
'db' => 'This is the ID of the DB application component.', |
155
|
|
|
'tableName' => 'This is the name of the DB table that the new ActiveRecord class is associated with, e.g. <code>post</code>. |
156
|
|
|
The table name may consist of the DB schema part if needed, e.g. <code>public.post</code>. |
157
|
|
|
The table name may end with asterisk to match multiple table names, e.g. <code>tbl_*</code> |
158
|
|
|
will match tables who name starts with <code>tbl_</code>. In this case, multiple ActiveRecord classes |
159
|
|
|
will be generated, one for each matching table name; and the class names will be generated from |
160
|
|
|
the matching characters. For example, table <code>tbl_post</code> will generate <code>Post</code> |
161
|
|
|
class.', |
162
|
|
|
'modelClass' => 'This is the name of the ActiveRecord class to be generated. The class name should not contain |
163
|
|
|
the namespace part as it is specified in "Namespace". You do not need to specify the class name |
164
|
|
|
if "Table Name" ends with asterisk, in which case multiple ActiveRecord classes will be generated.', |
165
|
|
|
'baseClass' => 'This is the base class of the new ActiveRecord class. It should be a fully qualified namespaced class name.', |
166
|
|
|
'guidAttribute' => 'REQUIRED. GUID Attribute Name, as well as field name of user table.', |
167
|
|
|
'idAttribute' => 'OPTIONAL. ID Attribute Name, as well as field name of user table. Please left empty if no need for this feature.', |
168
|
|
|
'passwordHashAttribute' => 'REQUIRED. Password Hash Attribute Name, as well as field name of user table.', |
169
|
|
|
'createdAtAttribute' => 'OPTIONAL. Created At Attribute Name, as well as field name of user table. Please left empty if no need for this feature.', |
170
|
|
|
'updatedAtAttribute' => 'OPTIONAL. Updated At Attribute Name, as well as field name of user table. Please left empty if no need for this feature.', |
171
|
|
|
'enableIP' => 'Indicates whether enable the IP address feature. If true, the following five attributes are required.', |
172
|
|
|
'ipAttribute1' => 'IP Attribute 1 Name, as well as field name of user table.', |
173
|
|
|
'ipAttribute2' => 'IP Attribute 2 Name, as well as field name of user table.', |
174
|
|
|
'ipAttribute3' => 'IP Attribute 3 Name, as well as field name of user table.', |
175
|
|
|
'ipAttribute4' => 'IP Attribute 4 Name, as well as field name of user table.', |
176
|
|
|
'ipTypeAttribute' => 'IP Type Attribute Name, as well as field name of user table.', |
177
|
|
|
'authKeyAttribute' => 'REQUIRED. Auth Key Attribute Name, as well as field name of user table.', |
178
|
|
|
'accessTokenAttribute' => 'REQUIRED. Access Token Attribute Name, as well as field name of user table.', |
179
|
|
|
'passwordResetTokenAttribute' => 'OPTIONAL. Password Reset Token Attribute Name, as well as field name of user table. Please left empty if no need for this feature.', |
180
|
|
|
'sourceAttribute' => 'OPTIONAL. Source Attribute Name, as well as field name of user table. Please left empty if no need for this feature.', |
181
|
|
|
'statusAttribute' => 'OPTIONAL. Status Attribute Name, as well as field name of user table. Please left empty if no need for this feature.', |
182
|
|
|
'generatePresettingRules' => 'This indicates whether the generator should generate the presetting rules.', |
183
|
|
|
'generatePresettingBehaviors' => 'This indicates whether the generator should generate the presetting behaviors.', |
184
|
|
|
'generateRelations' => 'This indicates whether the generator should generate relations based on |
185
|
|
|
foreign key constraints it detects in the database. Note that if your database contains too many tables, |
186
|
|
|
you may want to uncheck this option to accelerate the code generation process.', |
187
|
|
|
'generateLabelsFromComments' => 'This indicates whether the generator should generate attribute labels |
188
|
|
|
by using the comments of the corresponding DB columns.', |
189
|
|
|
'useTablePrefix' => 'This indicates whether the table name returned by the generated ActiveRecord class |
190
|
|
|
should consider the <code>tablePrefix</code> setting of the DB connection. For example, if the |
191
|
|
|
table name is <code>tbl_post</code> and <code>tablePrefix=tbl_</code>, the ActiveRecord class |
192
|
|
|
will return the table name as <code>{{%post}}</code>.', |
193
|
|
|
'useSchemaName' => 'This indicates whether to include the schema name in the ActiveRecord class |
194
|
|
|
when it\'s auto generated. Only non default schema would be used.', |
195
|
|
|
'generateQuery' => 'This indicates whether to generate ActiveQuery for the ActiveRecord class.', |
196
|
|
|
'queryNs' => 'This is the namespace of the ActiveQuery class to be generated, e.g., <code>app\models</code>', |
197
|
|
|
'queryClass' => 'This is the name of the ActiveQuery class to be generated. The class name should not contain |
198
|
|
|
the namespace part as it is specified in "ActiveQuery Namespace". You do not need to specify the class name |
199
|
|
|
if "Table Name" ends with asterisk, in which case multiple ActiveQuery classes will be generated.', |
200
|
|
|
'queryBaseClass' => 'This is the base class of the new ActiveQuery class. It should be a fully qualified namespaced class name.', |
201
|
|
|
]); |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* @inheritdoc |
206
|
|
|
*/ |
207
|
|
|
public function autoCompleteData() { |
208
|
|
|
$db = $this->getDbConnection(); |
209
|
|
|
if ($db !== null) { |
210
|
|
|
return [ |
211
|
|
|
'tableName' => function () use ($db) { |
212
|
|
|
return $db->getSchema()->getTableNames(); |
213
|
|
|
}, |
214
|
|
|
]; |
215
|
|
|
} else { |
216
|
|
|
return []; |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* @inheritdoc |
222
|
|
|
*/ |
223
|
|
|
public function requiredTemplates() { |
224
|
|
|
// @todo make 'query.php' to be required before 2.1 release |
225
|
|
|
return ['model.php'/* , 'query.php' */]; |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
* @inheritdoc |
230
|
|
|
*/ |
231
|
|
|
public function stickyAttributes() { |
232
|
|
|
return array_merge(parent::stickyAttributes(), ['ns', 'db', 'baseClass', 'generatePresettingRules', 'generatePresettingBehaviors', 'generateRelations', 'generateLabelsFromComments', 'useTablePrefix', 'queryNs', 'queryBaseClass']); |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
public function generate() { |
236
|
|
|
$files = []; |
237
|
|
|
$relations = $this->generateRelations(); |
238
|
|
|
$db = $this->getDbConnection(); |
239
|
|
|
foreach ($this->getTableNames() as $tableName) { |
240
|
|
|
// model : |
241
|
|
|
$modelClassName = $this->generateClassName($tableName); |
242
|
|
|
$queryClassName = ($this->generateQuery) ? $this->generateQueryClassName($modelClassName) : false; |
243
|
|
|
$tableSchema = $db->getTableSchema($tableName); |
244
|
|
|
$params = [ |
245
|
|
|
'tableName' => $tableName, |
246
|
|
|
'className' => $modelClassName, |
247
|
|
|
'queryClassName' => $queryClassName, |
248
|
|
|
'tableSchema' => $tableSchema, |
249
|
|
|
'labels' => $this->generateLabels($tableSchema), |
250
|
|
|
'rules' => $this->generatePresettingRules, |
251
|
|
|
'behaviors' => $this->generatePresettingBehaviors, |
252
|
|
|
'relations' => isset($relations[$tableName]) ? $relations[$tableName] : [], |
253
|
|
|
]; |
254
|
|
|
$files[] = new CodeFile( |
255
|
|
|
Yii::getAlias('@' . str_replace('\\', '/', $this->ns)) . '/' . $modelClassName . '.php', $this->render('model.php', $params) |
256
|
|
|
); |
257
|
|
|
|
258
|
|
|
// query : |
259
|
|
|
if ($queryClassName) { |
|
|
|
|
260
|
|
|
$params = [ |
261
|
|
|
'className' => $queryClassName, |
262
|
|
|
'modelClassName' => $modelClassName, |
263
|
|
|
]; |
264
|
|
|
$files[] = new CodeFile( |
265
|
|
|
Yii::getAlias('@' . str_replace('\\', '/', $this->queryNs)) . '/' . $queryClassName . '.php', $this->render('query.php', $params) |
266
|
|
|
); |
267
|
|
|
} |
268
|
|
|
} |
269
|
|
|
return $files; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Generates the attribute labels for the specified table. |
274
|
|
|
* @param \yii\db\TableSchema $table the table schema |
275
|
|
|
* @return array the generated attribute labels (name => label) |
276
|
|
|
*/ |
277
|
|
|
public function generateLabels($table) { |
278
|
|
|
$labels = []; |
279
|
|
|
foreach ($table->columns as $column) { |
280
|
|
|
if ($this->generateLabelsFromComments && !empty($column->comment)) { |
281
|
|
|
$labels[$column->name] = $column->comment; |
282
|
|
|
} elseif (!strcasecmp($column->name, 'id')) { |
283
|
|
|
$labels[$column->name] = 'ID'; |
284
|
|
|
} else { |
285
|
|
|
$label = Inflector::camel2words($column->name); |
286
|
|
|
if (!empty($label) && substr_compare($label, ' id', -3, 3, true) === 0) { |
287
|
|
|
$label = substr($label, 0, -3) . ' ID'; |
288
|
|
|
} |
289
|
|
|
$labels[$column->name] = $label; |
290
|
|
|
} |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
return $labels; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* Generates relations using a junction table by adding an extra viaTable(). |
298
|
|
|
* @param \yii\db\TableSchema the table being checked |
299
|
|
|
* @param array $fks obtained from the checkPivotTable() method |
300
|
|
|
* @param array $relations |
301
|
|
|
* @return array modified $relations |
302
|
|
|
*/ |
303
|
|
|
private function generateManyManyRelations($table, $fks, $relations) { |
304
|
|
|
$db = $this->getDbConnection(); |
305
|
|
|
$table0 = $fks[$table->primaryKey[0]][0]; |
306
|
|
|
$table1 = $fks[$table->primaryKey[1]][0]; |
307
|
|
|
$className0 = $this->generateClassName($table0); |
308
|
|
|
$className1 = $this->generateClassName($table1); |
309
|
|
|
$table0Schema = $db->getTableSchema($table0); |
310
|
|
|
$table1Schema = $db->getTableSchema($table1); |
311
|
|
|
|
312
|
|
|
$link = $this->generateRelationLink([$fks[$table->primaryKey[1]][1] => $table->primaryKey[1]]); |
313
|
|
|
$viaLink = $this->generateRelationLink([$table->primaryKey[0] => $fks[$table->primaryKey[0]][1]]); |
314
|
|
|
$relationName = $this->generateRelationName($relations, $table0Schema, $table->primaryKey[1], true); |
315
|
|
|
$relations[$table0Schema->fullName][$relationName] = [ |
316
|
|
|
"return \$this->hasMany($className1::className(), $link)->viaTable('" |
317
|
|
|
. $this->generateTableName($table->name) . "', $viaLink);", |
318
|
|
|
$className1, |
319
|
|
|
true, |
320
|
|
|
]; |
321
|
|
|
|
322
|
|
|
$link = $this->generateRelationLink([$fks[$table->primaryKey[0]][1] => $table->primaryKey[0]]); |
323
|
|
|
$viaLink = $this->generateRelationLink([$table->primaryKey[1] => $fks[$table->primaryKey[1]][1]]); |
324
|
|
|
$relationName = $this->generateRelationName($relations, $table1Schema, $table->primaryKey[0], true); |
325
|
|
|
$relations[$table1Schema->fullName][$relationName] = [ |
326
|
|
|
"return \$this->hasMany($className0::className(), $link)->viaTable('" |
327
|
|
|
. $this->generateTableName($table->name) . "', $viaLink);", |
328
|
|
|
$className0, |
329
|
|
|
true, |
330
|
|
|
]; |
331
|
|
|
|
332
|
|
|
return $relations; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* @return array the generated relation declarations |
337
|
|
|
*/ |
338
|
|
|
protected function generateRelations() { |
339
|
|
|
if (!$this->generateRelations) { |
340
|
|
|
return []; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
$db = $this->getDbConnection(); |
344
|
|
|
|
345
|
|
|
$schema = $db->getSchema(); |
346
|
|
|
if ($schema->hasMethod('getSchemaNames')) { // keep BC to Yii versions < 2.0.4 |
347
|
|
|
try { |
348
|
|
|
$schemaNames = $schema->getSchemaNames(); |
349
|
|
|
} catch (NotSupportedException $e) { |
350
|
|
|
// schema names are not supported by schema |
351
|
|
|
} |
352
|
|
|
} |
353
|
|
|
if (!isset($schemaNames)) { |
354
|
|
|
if (($pos = strpos($this->tableName, '.')) !== false) { |
355
|
|
|
$schemaNames = [substr($this->tableName, 0, $pos)]; |
356
|
|
|
} else { |
357
|
|
|
$schemaNames = ['']; |
358
|
|
|
} |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
$relations = []; |
362
|
|
|
foreach ($schemaNames as $schemaName) { |
363
|
|
|
foreach ($db->getSchema()->getTableSchemas($schemaName) as $table) { |
364
|
|
|
$className = $this->generateClassName($table->fullName); |
365
|
|
|
foreach ($table->foreignKeys as $refs) { |
366
|
|
|
$refTable = $refs[0]; |
367
|
|
|
$refTableSchema = $db->getTableSchema($refTable); |
368
|
|
|
unset($refs[0]); |
369
|
|
|
$fks = array_keys($refs); |
370
|
|
|
$refClassName = $this->generateClassName($refTable); |
371
|
|
|
|
372
|
|
|
// Add relation for this table |
373
|
|
|
$link = $this->generateRelationLink(array_flip($refs)); |
374
|
|
|
$relationName = $this->generateRelationName($relations, $table, $fks[0], false); |
375
|
|
|
$relations[$table->fullName][$relationName] = [ |
376
|
|
|
"return \$this->hasOne($refClassName::className(), $link);", |
377
|
|
|
$refClassName, |
378
|
|
|
false, |
379
|
|
|
]; |
380
|
|
|
|
381
|
|
|
// Add relation for the referenced table |
382
|
|
|
$uniqueKeys = [$table->primaryKey]; |
383
|
|
|
try { |
384
|
|
|
$uniqueKeys = array_merge($uniqueKeys, $db->getSchema()->findUniqueIndexes($table)); |
385
|
|
|
} catch (NotSupportedException $e) { |
386
|
|
|
// ignore |
387
|
|
|
} |
388
|
|
|
$hasMany = true; |
389
|
|
|
foreach ($uniqueKeys as $uniqueKey) { |
390
|
|
|
if (count(array_diff(array_merge($uniqueKey, $fks), array_intersect($uniqueKey, $fks))) === 0) { |
391
|
|
|
$hasMany = false; |
392
|
|
|
break; |
393
|
|
|
} |
394
|
|
|
} |
395
|
|
|
$link = $this->generateRelationLink($refs); |
396
|
|
|
$relationName = $this->generateRelationName($relations, $refTableSchema, $className, $hasMany); |
397
|
|
|
$relations[$refTableSchema->fullName][$relationName] = [ |
398
|
|
|
"return \$this->" . ($hasMany ? 'hasMany' : 'hasOne') . "($className::className(), $link);", |
399
|
|
|
$className, |
400
|
|
|
$hasMany, |
401
|
|
|
]; |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
if (($fks = $this->checkPivotTable($table)) === false) { |
405
|
|
|
continue; |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
$relations = $this->generateManyManyRelations($table, $fks, $relations); |
409
|
|
|
} |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
return $relations; |
413
|
|
|
} |
414
|
|
|
|
415
|
|
|
/** |
416
|
|
|
* Generates the link parameter to be used in generating the relation declaration. |
417
|
|
|
* @param array $refs reference constraint |
418
|
|
|
* @return string the generated link parameter. |
419
|
|
|
*/ |
420
|
|
|
protected function generateRelationLink($refs) { |
421
|
|
|
$pairs = []; |
422
|
|
|
foreach ($refs as $a => $b) { |
423
|
|
|
$pairs[] = "'$a' => '$b'"; |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
return '[' . implode(', ', $pairs) . ']'; |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
protected function generateRules($table) { |
430
|
|
|
|
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
/** |
434
|
|
|
* Checks if the given table is a junction table. |
435
|
|
|
* For simplicity, this method only deals with the case where the pivot contains two PK columns, |
436
|
|
|
* each referencing a column in a different table. |
437
|
|
|
* @param \yii\db\TableSchema the table being checked |
438
|
|
|
* @return array|boolean the relevant foreign key constraint information if the table is a junction table, |
439
|
|
|
* or false if the table is not a junction table. |
440
|
|
|
*/ |
441
|
|
|
protected function checkPivotTable($table) { |
442
|
|
|
$pk = $table->primaryKey; |
443
|
|
|
if (count($pk) !== 2) { |
444
|
|
|
return false; |
445
|
|
|
} |
446
|
|
|
$fks = []; |
447
|
|
|
foreach ($table->foreignKeys as $refs) { |
448
|
|
|
if (count($refs) === 2) { |
449
|
|
|
if (isset($refs[$pk[0]])) { |
450
|
|
|
$fks[$pk[0]] = [$refs[0], $refs[$pk[0]]]; |
451
|
|
|
} elseif (isset($refs[$pk[1]])) { |
452
|
|
|
$fks[$pk[1]] = [$refs[0], $refs[$pk[1]]]; |
453
|
|
|
} |
454
|
|
|
} |
455
|
|
|
} |
456
|
|
|
if (count($fks) === 2 && $fks[$pk[0]][0] !== $fks[$pk[1]][0]) { |
457
|
|
|
return $fks; |
458
|
|
|
} else { |
459
|
|
|
return false; |
460
|
|
|
} |
461
|
|
|
} |
462
|
|
|
|
463
|
|
|
/** |
464
|
|
|
* Generate a relation name for the specified table and a base name. |
465
|
|
|
* @param array $relations the relations being generated currently. |
466
|
|
|
* @param \yii\db\TableSchema $table the table schema |
467
|
|
|
* @param string $key a base name that the relation name may be generated from |
468
|
|
|
* @param boolean $multiple whether this is a has-many relation |
469
|
|
|
* @return string the relation name |
470
|
|
|
*/ |
471
|
|
|
protected function generateRelationName($relations, $table, $key, $multiple) { |
472
|
|
|
if (!empty($key) && substr_compare($key, 'id', -2, 2, true) === 0 && strcasecmp($key, 'id')) { |
473
|
|
|
$key = rtrim(substr($key, 0, -2), '_'); |
474
|
|
|
} |
475
|
|
|
if ($multiple) { |
476
|
|
|
$key = Inflector::pluralize($key); |
477
|
|
|
} |
478
|
|
|
$name = $rawName = Inflector::id2camel($key, '_'); |
479
|
|
|
$i = 0; |
480
|
|
|
while (isset($table->columns[lcfirst($name)])) { |
481
|
|
|
$name = $rawName . ($i++); |
482
|
|
|
} |
483
|
|
|
while (isset($relations[$table->fullName][$name])) { |
484
|
|
|
$name = $rawName . ($i++); |
485
|
|
|
} |
486
|
|
|
|
487
|
|
|
return $name; |
488
|
|
|
} |
489
|
|
|
|
490
|
|
|
public function validateFieldsExist() { |
491
|
|
|
$db = $this->getDbConnection(); |
492
|
|
|
foreach ($this->getTableNames() as $table) { |
493
|
|
|
$tableSchema = $db->getTableSchema($table); |
494
|
|
|
$fields = [ |
495
|
|
|
'guidAttribute', |
496
|
|
|
'idAttribute', |
497
|
|
|
'passwordHashAttribute', |
498
|
|
|
'createdAtAttribute', |
499
|
|
|
'updatedAtAttribute', |
500
|
|
|
'ipAttribute1', |
501
|
|
|
'ipAttribute2', |
502
|
|
|
'ipAttribute3', |
503
|
|
|
'ipAttribute4', |
504
|
|
|
'ipTypeAttribute', |
505
|
|
|
'authKeyAttribute', |
506
|
|
|
'accessTokenAttribute', |
507
|
|
|
'passwordResetTokenAttribute', |
508
|
|
|
'statusAttribute', |
509
|
|
|
'sourceAttribute', |
510
|
|
|
]; |
511
|
|
|
foreach ($fields as $field) { |
512
|
|
|
if (isset($this->$field) && $this->$field != '' && !isset($tableSchema->columns[$this->$field])) { |
513
|
|
|
$this->addError($field, '`' . $this->$field . '` field does not exist in `' . $table . '` table.'); |
514
|
|
|
continue; |
515
|
|
|
} |
516
|
|
|
} |
517
|
|
|
} |
518
|
|
|
} |
519
|
|
|
|
520
|
|
|
/** |
521
|
|
|
* Validates the [[db]] attribute. |
522
|
|
|
*/ |
523
|
|
|
public function validateDb() { |
524
|
|
|
if (!Yii::$app->has($this->db)) { |
525
|
|
|
$this->addError('db', 'There is no application component named "db".'); |
526
|
|
|
} elseif (!Yii::$app->get($this->db) instanceof Connection) { |
527
|
|
|
$this->addError('db', 'The "db" application component must be a DB connection instance.'); |
528
|
|
|
} |
529
|
|
|
} |
530
|
|
|
|
531
|
|
|
public function validateIP() { |
532
|
|
|
if (!$this->enableIP) { |
533
|
|
|
return; |
534
|
|
|
} |
535
|
|
|
$db = $this->getDbConnection(); |
|
|
|
|
536
|
|
|
$ipAttribute = 'ipAttribute'; |
537
|
|
|
for ($i = 1; $i <= 4; $i++) { |
538
|
|
|
$ipAttributeName = $ipAttribute . $i; |
539
|
|
|
if (empty($this->$ipAttributeName)) { |
540
|
|
|
$this->addError($ipAttributeName, "`$ipAttributeName` should not be empty."); |
541
|
|
|
continue; |
542
|
|
|
} |
543
|
|
|
} |
544
|
|
|
if (empty($this->ipTypeAttribute)) { |
545
|
|
|
$this->addError('ipTypeAttribute', "`ipTypeAttribute` should not be empty."); |
546
|
|
|
return; |
547
|
|
|
} |
548
|
|
|
} |
549
|
|
|
|
550
|
|
|
/** |
551
|
|
|
* Validates the [[ns]] attribute. |
552
|
|
|
*/ |
553
|
|
|
public function validateNamespace() { |
554
|
|
|
$this->ns = ltrim($this->ns, '\\'); |
555
|
|
|
$path = Yii::getAlias('@' . str_replace('\\', '/', $this->ns), false); |
556
|
|
|
if ($path === false) { |
557
|
|
|
$this->addError('ns', 'Namespace must be associated with an existing directory.'); |
558
|
|
|
} |
559
|
|
|
} |
560
|
|
|
|
561
|
|
|
/** |
562
|
|
|
* Validates the [[modelClass]] attribute. |
563
|
|
|
*/ |
564
|
|
|
public function validateModelClass() { |
565
|
|
|
if ($this->isReservedKeyword($this->modelClass)) { |
566
|
|
|
$this->addError('modelClass', 'Class name cannot be a reserved PHP keyword.'); |
567
|
|
|
} |
568
|
|
|
if ((empty($this->tableName) || substr_compare($this->tableName, '*', -1, 1)) && $this->modelClass == '') { |
569
|
|
|
$this->addError('modelClass', 'Model Class cannot be blank if table name does not end with asterisk.'); |
570
|
|
|
} |
571
|
|
|
} |
572
|
|
|
|
573
|
|
|
/** |
574
|
|
|
* Validates the [[tableName]] attribute. |
575
|
|
|
*/ |
576
|
|
|
public function validateTableName() { |
577
|
|
|
if (strpos($this->tableName, '*') !== false && substr_compare($this->tableName, '*', -1, 1)) { |
578
|
|
|
$this->addError('tableName', 'Asterisk is only allowed as the last character.'); |
579
|
|
|
|
580
|
|
|
return; |
581
|
|
|
} |
582
|
|
|
$tables = $this->getTableNames(); |
583
|
|
|
if (empty($tables)) { |
584
|
|
|
$this->addError('tableName', "Table '{$this->tableName}' does not exist."); |
585
|
|
|
} else { |
586
|
|
|
foreach ($tables as $table) { |
587
|
|
|
$class = $this->generateClassName($table); |
588
|
|
|
if ($this->isReservedKeyword($class)) { |
589
|
|
|
$this->addError('tableName', "Table '$table' will generate a class which is a reserved PHP keyword."); |
590
|
|
|
break; |
591
|
|
|
} |
592
|
|
|
} |
593
|
|
|
} |
594
|
|
|
} |
595
|
|
|
|
596
|
|
|
protected $tableNames; |
597
|
|
|
protected $classNames; |
598
|
|
|
|
599
|
|
|
/** |
600
|
|
|
* @return array the table names that match the pattern specified by [[tableName]]. |
601
|
|
|
*/ |
602
|
|
|
protected function getTableNames() { |
603
|
|
|
if ($this->tableNames !== null) { |
604
|
|
|
return $this->tableNames; |
605
|
|
|
} |
606
|
|
|
$db = $this->getDbConnection(); |
607
|
|
|
if ($db === null) { |
608
|
|
|
return []; |
609
|
|
|
} |
610
|
|
|
$tableNames = []; |
611
|
|
|
if (strpos($this->tableName, '*') !== false) { |
612
|
|
|
if (($pos = strrpos($this->tableName, '.')) !== false) { |
613
|
|
|
$schema = substr($this->tableName, 0, $pos); |
614
|
|
|
$pattern = '/^' . str_replace('*', '\w+', substr($this->tableName, $pos + 1)) . '$/'; |
615
|
|
|
} else { |
616
|
|
|
$schema = ''; |
617
|
|
|
$pattern = '/^' . str_replace('*', '\w+', $this->tableName) . '$/'; |
618
|
|
|
} |
619
|
|
|
|
620
|
|
|
foreach ($db->schema->getTableNames($schema) as $table) { |
621
|
|
|
if (preg_match($pattern, $table)) { |
622
|
|
|
$tableNames[] = $schema === '' ? $table : ($schema . '.' . $table); |
623
|
|
|
} |
624
|
|
|
} |
625
|
|
|
} elseif (($table = $db->getTableSchema($this->tableName, true)) !== null) { |
626
|
|
|
$tableNames[] = $this->tableName; |
627
|
|
|
$this->classNames[$this->tableName] = $this->modelClass; |
628
|
|
|
} |
629
|
|
|
|
630
|
|
|
return $this->tableNames = $tableNames; |
631
|
|
|
} |
632
|
|
|
|
633
|
|
|
/** |
634
|
|
|
* Generates the table name by considering table prefix. |
635
|
|
|
* If [[useTablePrefix]] is false, the table name will be returned without change. |
636
|
|
|
* @param string $tableName the table name (which may contain schema prefix) |
637
|
|
|
* @return string the generated table name |
638
|
|
|
*/ |
639
|
|
|
public function generateTableName($tableName) { |
640
|
|
|
if (!$this->useTablePrefix) { |
641
|
|
|
return $tableName; |
642
|
|
|
} |
643
|
|
|
|
644
|
|
|
$db = $this->getDbConnection(); |
645
|
|
|
if (preg_match("/^{$db->tablePrefix}(.*?)$/", $tableName, $matches)) { |
646
|
|
|
$tableName = '{{%' . $matches[1] . '}}'; |
647
|
|
|
} elseif (preg_match("/^(.*?){$db->tablePrefix}$/", $tableName, $matches)) { |
648
|
|
|
$tableName = '{{' . $matches[1] . '%}}'; |
649
|
|
|
} |
650
|
|
|
return $tableName; |
651
|
|
|
} |
652
|
|
|
|
653
|
|
|
/** |
654
|
|
|
* Generates a class name from the specified table name. |
655
|
|
|
* @param string $tableName the table name (which may contain schema prefix) |
656
|
|
|
* @param boolean $useSchemaName should schema name be included in the class name, if present |
657
|
|
|
* @return string the generated class name |
658
|
|
|
*/ |
659
|
|
|
protected function generateClassName($tableName, $useSchemaName = null) { |
660
|
|
|
if (isset($this->classNames[$tableName])) { |
661
|
|
|
return $this->classNames[$tableName]; |
662
|
|
|
} |
663
|
|
|
|
664
|
|
|
$schemaName = ''; |
665
|
|
|
$fullTableName = $tableName; |
666
|
|
|
if (($pos = strrpos($tableName, '.')) !== false) { |
667
|
|
|
if (($useSchemaName === null && $this->useSchemaName) || $useSchemaName) { |
668
|
|
|
$schemaName = substr($tableName, 0, $pos) . '_'; |
669
|
|
|
} |
670
|
|
|
$tableName = substr($tableName, $pos + 1); |
671
|
|
|
} |
672
|
|
|
|
673
|
|
|
$db = $this->getDbConnection(); |
674
|
|
|
$patterns = []; |
675
|
|
|
$patterns[] = "/^{$db->tablePrefix}(.*?)$/"; |
676
|
|
|
$patterns[] = "/^(.*?){$db->tablePrefix}$/"; |
677
|
|
|
if (strpos($this->tableName, '*') !== false) { |
678
|
|
|
$pattern = $this->tableName; |
679
|
|
|
if (($pos = strrpos($pattern, '.')) !== false) { |
680
|
|
|
$pattern = substr($pattern, $pos + 1); |
681
|
|
|
} |
682
|
|
|
$patterns[] = '/^' . str_replace('*', '(\w+)', $pattern) . '$/'; |
683
|
|
|
} |
684
|
|
|
$className = $tableName; |
685
|
|
|
foreach ($patterns as $pattern) { |
686
|
|
|
if (preg_match($pattern, $tableName, $matches)) { |
687
|
|
|
$className = $matches[1]; |
688
|
|
|
break; |
689
|
|
|
} |
690
|
|
|
} |
691
|
|
|
|
692
|
|
|
return $this->classNames[$fullTableName] = Inflector::id2camel($schemaName . $className, '_'); |
693
|
|
|
} |
694
|
|
|
|
695
|
|
|
/** |
696
|
|
|
* Generates a query class name from the specified model class name. |
697
|
|
|
* @param string $modelClassName model class name |
698
|
|
|
* @return string generated class name |
699
|
|
|
*/ |
700
|
|
|
protected function generateQueryClassName($modelClassName) { |
701
|
|
|
$queryClassName = $this->queryClass; |
702
|
|
|
if (empty($queryClassName) || strpos($this->tableName, '*') !== false) { |
703
|
|
|
$queryClassName = $modelClassName . 'Query'; |
704
|
|
|
} |
705
|
|
|
return $queryClassName; |
706
|
|
|
} |
707
|
|
|
|
708
|
|
|
/** |
709
|
|
|
* @return Connection the DB connection as specified by [[db]]. |
710
|
|
|
*/ |
711
|
|
|
protected function getDbConnection() { |
712
|
|
|
return Yii::$app->get($this->db, false); |
713
|
|
|
} |
714
|
|
|
|
715
|
|
|
/** |
716
|
|
|
* Checks if any of the specified columns is auto incremental. |
717
|
|
|
* @param \yii\db\TableSchema $table the table schema |
718
|
|
|
* @param array $columns columns to check for autoIncrement property |
719
|
|
|
* @return boolean whether any of the specified columns is auto incremental. |
720
|
|
|
*/ |
721
|
|
|
protected function isColumnAutoIncremental($table, $columns) { |
722
|
|
|
foreach ($columns as $column) { |
723
|
|
|
if (isset($table->columns[$column]) && $table->columns[$column]->autoIncrement) { |
724
|
|
|
return true; |
725
|
|
|
} |
726
|
|
|
} |
727
|
|
|
|
728
|
|
|
return false; |
729
|
|
|
} |
730
|
|
|
|
731
|
|
|
} |
732
|
|
|
|
In PHP, under loose comparison (like
==
, or!=
, orswitch
conditions), values of different types might be equal.For
string
values, the empty string''
is a special case, in particular the following results might be unexpected: