@flarum/cli 3.0.12 → 3.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -1
- package/boilerplate/skeleton/extension/composer.json +4 -0
- package/boilerplate/skeleton/extension/js/package.json +5 -5
- package/boilerplate/skeleton/extension/js/tsconfig.json +3 -0
- package/boilerplate/skeleton/extension/js/tsconfig.test.json +1 -0
- package/boilerplate/stubs/backend/advanced-api-resource.php +3 -3
- package/boilerplate/stubs/backend/model-factory.php +17 -0
- package/boilerplate/stubs/backend/model.php +3 -0
- package/lib/base-command.d.ts +21 -3
- package/lib/base-command.js +52 -5
- package/lib/commands/make/backend/model.js +10 -0
- package/lib/commands/upgrade/2.0.d.ts +43 -0
- package/lib/commands/upgrade/2.0.js +128 -2
- package/lib/steps/gen-ext-scaffolder.d.ts +5 -0
- package/lib/steps/gen-ext-scaffolder.js +17 -2
- package/lib/steps/stubs/backend/advanced-api-resource.js +1 -1
- package/lib/steps/stubs/backend/model-factory.d.ts +24 -0
- package/lib/steps/stubs/backend/model-factory.js +33 -0
- package/lib/steps/upgrade/twopointoh/backend/email-views.js +22 -30
- package/lib/steps/upgrade/twopointoh/backend/extension-models.d.ts +30 -0
- package/lib/steps/upgrade/twopointoh/backend/extension-models.js +34 -0
- package/lib/steps/upgrade/twopointoh/backend/json-api.d.ts +6 -0
- package/lib/steps/upgrade/twopointoh/backend/json-api.js +14 -0
- package/lib/steps/upgrade/twopointoh/backend/misc.d.ts +6 -0
- package/lib/steps/upgrade/twopointoh/backend/misc.js +19 -7
- package/lib/steps/upgrade/twopointoh/backend/model-factories.d.ts +18 -0
- package/lib/steps/upgrade/twopointoh/backend/model-factories.js +73 -0
- package/lib/steps/upgrade/twopointoh/backend/phpunit.d.ts +2 -0
- package/lib/steps/upgrade/twopointoh/backend/phpunit.js +16 -8
- package/lib/steps/upgrade/twopointoh/backend/search.d.ts +9 -0
- package/lib/steps/upgrade/twopointoh/backend/search.js +28 -10
- package/lib/steps/upgrade/twopointoh/base.d.ts +38 -0
- package/lib/steps/upgrade/twopointoh/base.js +73 -12
- package/lib/steps/upgrade/twopointoh/dependencies.d.ts +55 -0
- package/lib/steps/upgrade/twopointoh/dependencies.js +317 -20
- package/lib/steps/upgrade/twopointoh/frontend/admin-extender.d.ts +86 -0
- package/lib/steps/upgrade/twopointoh/frontend/admin-extender.js +842 -0
- package/lib/steps/upgrade/twopointoh/frontend/code-splitting/usage-of-lazy-modules.js +110 -18
- package/lib/steps/upgrade/twopointoh/frontend/export-registry/import-ext.js +13 -16
- package/lib/steps/upgrade/twopointoh/frontend/misc.d.ts +23 -0
- package/lib/steps/upgrade/twopointoh/frontend/misc.js +147 -8
- package/lib/steps/upgrade/twopointoh/infra.d.ts +16 -1
- package/lib/steps/upgrade/twopointoh/infra.js +87 -5
- package/lib/steps/upgrade/twopointoh/step-failures.d.ts +8 -0
- package/lib/steps/upgrade/twopointoh/step-failures.js +6 -0
- package/lib/steps/upgrade/twopointoh/todos.d.ts +22 -0
- package/lib/steps/upgrade/twopointoh/todos.js +57 -0
- package/lib/steps/upgrade/twopointoh/typescript-5-9.d.ts +15 -0
- package/lib/steps/upgrade/twopointoh/typescript-5-9.js +250 -0
- package/lib/utils/ast.js +3 -2
- package/lib/utils/babel-ts.d.ts +9 -0
- package/lib/utils/babel-ts.js +18 -0
- package/lib/utils/composer.d.ts +5 -0
- package/lib/utils/composer.js +10 -1
- package/lib/utils/terminal.d.ts +13 -0
- package/lib/utils/terminal.js +26 -0
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/php-subsystem/index.php +12 -0
- package/php-subsystem/src/NodeUtil.php +19 -9
- package/php-subsystem/src/NodeVisitors/AddExtender.php +49 -1
- package/php-subsystem/src/Upgrade/TwoPointOh/JsonApi.php +2 -20
- package/php-subsystem/src/Upgrade/TwoPointOh/Misc.php +13 -17
- package/php-subsystem/src/Upgrade/TwoPointOh/ModelFactories.php +943 -0
- package/php-subsystem/src/Upgrade/TwoPointOh/PhpUnit.php +476 -71
- package/php-subsystem/src/Upgrade/TwoPointOh/Search.php +97 -13
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Flarum\CliPhpSubsystem\Upgrade\TwoPointOh;
|
|
4
|
+
|
|
5
|
+
use PhpParser\Node;
|
|
6
|
+
use PhpParser\NodeTraverser;
|
|
7
|
+
use PhpParser\NodeVisitor;
|
|
8
|
+
use PhpParser\NodeVisitorAbstract;
|
|
9
|
+
use PhpParser\ParserFactory;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Factories for an extension's models, written from what its migrations say
|
|
13
|
+
* about each model's table.
|
|
14
|
+
*/
|
|
15
|
+
class ModelFactories
|
|
16
|
+
{
|
|
17
|
+
/**
|
|
18
|
+
* Blueprint methods that add a column, by the kind of value it holds.
|
|
19
|
+
*/
|
|
20
|
+
protected const COLUMN_TYPES = [
|
|
21
|
+
'increments' => 'int', 'bigIncrements' => 'int', 'mediumIncrements' => 'int', 'smallIncrements' => 'int', 'tinyIncrements' => 'int', 'id' => 'int',
|
|
22
|
+
'integer' => 'int', 'bigInteger' => 'int', 'mediumInteger' => 'int', 'smallInteger' => 'int', 'tinyInteger' => 'int',
|
|
23
|
+
'unsignedInteger' => 'int', 'unsignedBigInteger' => 'int', 'unsignedMediumInteger' => 'int', 'unsignedSmallInteger' => 'int', 'unsignedTinyInteger' => 'int',
|
|
24
|
+
'foreignId' => 'int', 'year' => 'int',
|
|
25
|
+
'string' => 'string', 'char' => 'string', 'macAddress' => 'string', 'ulid' => 'string',
|
|
26
|
+
'text' => 'text', 'tinyText' => 'text', 'mediumText' => 'text', 'longText' => 'text',
|
|
27
|
+
'boolean' => 'bool',
|
|
28
|
+
'dateTime' => 'datetime', 'dateTimeTz' => 'datetime', 'timestamp' => 'datetime', 'timestampTz' => 'datetime',
|
|
29
|
+
'date' => 'datetime', 'time' => 'datetime', 'timeTz' => 'datetime',
|
|
30
|
+
'json' => 'json', 'jsonb' => 'json',
|
|
31
|
+
'decimal' => 'number', 'unsignedDecimal' => 'number', 'float' => 'number', 'double' => 'number',
|
|
32
|
+
'enum' => 'enum', 'set' => 'enum',
|
|
33
|
+
'uuid' => 'uuid',
|
|
34
|
+
'ipAddress' => 'ip',
|
|
35
|
+
'binary' => 'binary',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
protected const AUTO_INCREMENTS = ['increments', 'bigIncrements', 'mediumIncrements', 'smallIncrements', 'tinyIncrements', 'id'];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Each table's columns, by name, in the order they were added.
|
|
42
|
+
*
|
|
43
|
+
* @var array<string, array<string, array>>
|
|
44
|
+
*/
|
|
45
|
+
protected array $tables = [];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Replay the migrations' `up`s, in the order Flarum runs them, and return
|
|
49
|
+
* the tables they leave.
|
|
50
|
+
*
|
|
51
|
+
* @param array<string, string> $migrations Each migration's code, by file name.
|
|
52
|
+
*/
|
|
53
|
+
public function schema(array $migrations): array
|
|
54
|
+
{
|
|
55
|
+
ksort($migrations);
|
|
56
|
+
|
|
57
|
+
$this->tables = [];
|
|
58
|
+
$parser = (new ParserFactory())->createForHostVersion();
|
|
59
|
+
|
|
60
|
+
foreach ($migrations as $code) {
|
|
61
|
+
foreach ($parser->parse($code) ?? [] as $statement) {
|
|
62
|
+
if ($statement instanceof Node\Stmt\Return_ && $statement->expr) {
|
|
63
|
+
$this->migration($statement->expr);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return ['tables' => (object) array_map('array_values', $this->tables)];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
protected function migration(Node\Expr $migration): void
|
|
72
|
+
{
|
|
73
|
+
if ($migration instanceof Node\Expr\StaticCall && $migration->class instanceof Node\Name && $migration->class->getLast() === 'Migration' && $migration->name instanceof Node\Identifier) {
|
|
74
|
+
$this->helper($migration->name->toString(), array_map(fn (Node\Arg $arg) => $arg->value, $migration->getArgs()));
|
|
75
|
+
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if ($migration instanceof Node\Expr\Array_) {
|
|
80
|
+
foreach ($migration->items as $item) {
|
|
81
|
+
if ($item && $this->literal($item->key) === 'up' && ($item->value instanceof Node\Expr\Closure || $item->value instanceof Node\Expr\ArrowFunction)) {
|
|
82
|
+
$this->up($item->value);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* What a `Flarum\Database\Migration` helper does.
|
|
90
|
+
*/
|
|
91
|
+
protected function helper(string $name, array $args): void
|
|
92
|
+
{
|
|
93
|
+
$table = $this->literal($args[0] ?? null);
|
|
94
|
+
|
|
95
|
+
if (! is_string($table)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
switch ($name) {
|
|
100
|
+
case 'createTable':
|
|
101
|
+
case 'createTableIfNotExists':
|
|
102
|
+
$this->tables[$table] = [];
|
|
103
|
+
|
|
104
|
+
if (isset($args[1])) {
|
|
105
|
+
$this->blueprint($table, $args[1]);
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case 'renameTable':
|
|
110
|
+
$this->renameTable($table, $this->literal($args[1] ?? null));
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case 'addColumns':
|
|
114
|
+
case 'dropColumns':
|
|
115
|
+
foreach ($args[1]->items ?? [] as $item) {
|
|
116
|
+
$column = $this->literal($item?->key);
|
|
117
|
+
|
|
118
|
+
if (! is_string($column)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if ($name === 'dropColumns') {
|
|
123
|
+
unset($this->tables[$table][$column]);
|
|
124
|
+
} elseif ($item->value instanceof Node\Expr\Array_) {
|
|
125
|
+
$this->addColumn($table, $column, $item->value);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
|
|
130
|
+
case 'renameColumn':
|
|
131
|
+
$this->renameColumn($table, $this->literal($args[1] ?? null), $this->literal($args[2] ?? null));
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
case 'renameColumns':
|
|
135
|
+
foreach ($args[1]->items ?? [] as $item) {
|
|
136
|
+
$this->renameColumn($table, $this->literal($item?->key), $this->literal($item?->value));
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* What the schema builder does in an `up`, in order.
|
|
144
|
+
*/
|
|
145
|
+
protected function up(Node\Expr\Closure|Node\Expr\ArrowFunction $up): void
|
|
146
|
+
{
|
|
147
|
+
$traverser = new NodeTraverser();
|
|
148
|
+
|
|
149
|
+
$traverser->addVisitor(new class ($this) extends NodeVisitorAbstract {
|
|
150
|
+
public function __construct(private ModelFactories $factories)
|
|
151
|
+
{
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public function enterNode(Node $node)
|
|
155
|
+
{
|
|
156
|
+
if (! $node instanceof Node\Expr\MethodCall || ! $node->name instanceof Node\Identifier) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
$args = array_map(fn (Node\Arg $arg) => $arg->value, $node->getArgs());
|
|
161
|
+
|
|
162
|
+
return $this->factories->builder($node->name->toString(), $args) ? NodeVisitor::DONT_TRAVERSE_CHILDREN : null;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
$traverser->traverse($up instanceof Node\Expr\Closure ? $up->stmts : [new Node\Stmt\Expression($up->expr)]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A schema builder call, if it is one that changes a table.
|
|
171
|
+
*/
|
|
172
|
+
public function builder(string $method, array $args): bool
|
|
173
|
+
{
|
|
174
|
+
$table = $this->literal($args[0] ?? null);
|
|
175
|
+
$callback = $args[1] ?? null;
|
|
176
|
+
|
|
177
|
+
if (! is_string($table)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (in_array($method, ['create', 'table']) && ($callback instanceof Node\Expr\Closure || $callback instanceof Node\Expr\ArrowFunction)) {
|
|
182
|
+
if ($method === 'create') {
|
|
183
|
+
$this->tables[$table] = [];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
$this->blueprint($table, $callback);
|
|
187
|
+
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if ($method === 'rename' && count($args) === 2) {
|
|
192
|
+
$this->renameTable($table, $this->literal($args[1]));
|
|
193
|
+
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (in_array($method, ['drop', 'dropIfExists']) && count($args) === 1) {
|
|
198
|
+
unset($this->tables[$table]);
|
|
199
|
+
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* What a Blueprint callback does to its table.
|
|
208
|
+
*/
|
|
209
|
+
protected function blueprint(string $table, Node\Expr $callback): void
|
|
210
|
+
{
|
|
211
|
+
$this->tables[$table] ??= [];
|
|
212
|
+
|
|
213
|
+
$param = $callback->params[0]->var->name ?? null;
|
|
214
|
+
|
|
215
|
+
if (! is_string($param) || ! ($callback instanceof Node\Expr\Closure || $callback instanceof Node\Expr\ArrowFunction)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
$statements = $callback instanceof Node\Expr\Closure ? $callback->stmts : [new Node\Stmt\Expression($callback->expr)];
|
|
220
|
+
|
|
221
|
+
$traverser = new NodeTraverser();
|
|
222
|
+
|
|
223
|
+
$traverser->addVisitor(new class ($this, $table, $param) extends NodeVisitorAbstract {
|
|
224
|
+
public function __construct(private ModelFactories $factories, private string $table, private string $param)
|
|
225
|
+
{
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
public function enterNode(Node $node)
|
|
229
|
+
{
|
|
230
|
+
if (! $node instanceof Node\Stmt\Expression) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// $table->string('name')->nullable(): the call on $table, then its modifiers.
|
|
235
|
+
$calls = [];
|
|
236
|
+
$expr = $node->expr;
|
|
237
|
+
|
|
238
|
+
while ($expr instanceof Node\Expr\MethodCall && $expr->name instanceof Node\Identifier) {
|
|
239
|
+
array_unshift($calls, $expr);
|
|
240
|
+
$expr = $expr->var;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if ($calls && $expr instanceof Node\Expr\Variable && $expr->name === $this->param) {
|
|
244
|
+
$this->factories->column($this->table, $calls);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return NodeVisitor::DONT_TRAVERSE_CHILDREN;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
$traverser->traverse($statements);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* What one chain of Blueprint calls does to the table.
|
|
256
|
+
*
|
|
257
|
+
* @param Node\Expr\MethodCall[] $calls
|
|
258
|
+
*/
|
|
259
|
+
public function column(string $table, array $calls): void
|
|
260
|
+
{
|
|
261
|
+
$method = $calls[0]->name->toString();
|
|
262
|
+
$args = array_map(fn (Node\Arg $arg) => $arg->value, $calls[0]->getArgs());
|
|
263
|
+
$modifiers = array_slice($calls, 1);
|
|
264
|
+
|
|
265
|
+
switch ($method) {
|
|
266
|
+
case 'timestamps':
|
|
267
|
+
case 'nullableTimestamps':
|
|
268
|
+
case 'timestampsTz':
|
|
269
|
+
$this->define($table, ['name' => 'created_at', 'type' => 'datetime', 'nullable' => true]);
|
|
270
|
+
$this->define($table, ['name' => 'updated_at', 'type' => 'datetime', 'nullable' => true]);
|
|
271
|
+
|
|
272
|
+
return;
|
|
273
|
+
|
|
274
|
+
case 'softDeletes':
|
|
275
|
+
case 'softDeletesTz':
|
|
276
|
+
$this->define($table, ['name' => $this->literal($args[0] ?? null) ?? 'deleted_at', 'type' => 'datetime', 'nullable' => true]);
|
|
277
|
+
|
|
278
|
+
return;
|
|
279
|
+
|
|
280
|
+
case 'rememberToken':
|
|
281
|
+
$this->define($table, ['name' => 'remember_token', 'type' => 'string', 'nullable' => true]);
|
|
282
|
+
|
|
283
|
+
return;
|
|
284
|
+
|
|
285
|
+
case 'morphs':
|
|
286
|
+
case 'nullableMorphs':
|
|
287
|
+
$name = $this->literal($args[0] ?? null);
|
|
288
|
+
|
|
289
|
+
if (is_string($name)) {
|
|
290
|
+
$this->define($table, ['name' => "{$name}_type", 'type' => 'string', 'nullable' => $method === 'nullableMorphs']);
|
|
291
|
+
$this->define($table, ['name' => "{$name}_id", 'type' => 'int', 'nullable' => $method === 'nullableMorphs']);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return;
|
|
295
|
+
|
|
296
|
+
case 'addColumn':
|
|
297
|
+
$type = $this->literal($args[0] ?? null);
|
|
298
|
+
$name = $this->literal($args[1] ?? null);
|
|
299
|
+
|
|
300
|
+
if (is_string($type) && is_string($name)) {
|
|
301
|
+
$this->addColumn($table, $name, new Node\Expr\Array_([
|
|
302
|
+
new Node\ArrayItem(new Node\Scalar\String_($type)),
|
|
303
|
+
...(($args[2] ?? null) instanceof Node\Expr\Array_ ? $args[2]->items : []),
|
|
304
|
+
]));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return;
|
|
308
|
+
|
|
309
|
+
case 'foreign':
|
|
310
|
+
$on = $this->modifierArgument($modifiers, 'on');
|
|
311
|
+
|
|
312
|
+
foreach ($this->names($args[0] ?? null) as $name) {
|
|
313
|
+
if (is_string($on) && isset($this->tables[$table][$name])) {
|
|
314
|
+
$this->tables[$table][$name]['references'] = $on;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return;
|
|
319
|
+
|
|
320
|
+
case 'dropForeign':
|
|
321
|
+
foreach ($this->names($args[0] ?? null) as $name) {
|
|
322
|
+
// An index name, `{table}_{column}_foreign`, or the column.
|
|
323
|
+
$name = preg_replace('/^'.preg_quote($table, '/').'_(.+)_foreign$/', '$1', $name);
|
|
324
|
+
|
|
325
|
+
if (isset($this->tables[$table][$name])) {
|
|
326
|
+
$this->tables[$table][$name]['references'] = null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return;
|
|
331
|
+
|
|
332
|
+
case 'dropColumn':
|
|
333
|
+
foreach ($args as $arg) {
|
|
334
|
+
foreach ($this->names($arg) as $name) {
|
|
335
|
+
unset($this->tables[$table][$name]);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return;
|
|
340
|
+
|
|
341
|
+
case 'renameColumn':
|
|
342
|
+
$this->renameColumn($table, $this->literal($args[0] ?? null), $this->literal($args[1] ?? null));
|
|
343
|
+
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (! isset(self::COLUMN_TYPES[$method])) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
$column = [
|
|
352
|
+
'name' => $method === 'id' ? ($this->literal($args[0] ?? null) ?? 'id') : $this->literal($args[0] ?? null),
|
|
353
|
+
'type' => self::COLUMN_TYPES[$method],
|
|
354
|
+
'nullable' => false,
|
|
355
|
+
'autoIncrement' => in_array($method, self::AUTO_INCREMENTS)
|
|
356
|
+
// integer('id', true)
|
|
357
|
+
|| (self::COLUMN_TYPES[$method] === 'int' && $this->literal($args[1] ?? null) === true),
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
if ($column['type'] === 'enum') {
|
|
361
|
+
$column['values'] = array_values(array_filter(array_map(fn ($item) => $this->literal($item?->value), $args[1]->items ?? []), 'is_string'));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
foreach ($modifiers as $modifier) {
|
|
365
|
+
$column = $this->modify($column, $modifier);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (is_string($column['name'])) {
|
|
369
|
+
$this->define($table, $column);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* A column with one modifier applied.
|
|
375
|
+
*/
|
|
376
|
+
protected function modify(array $column, Node\Expr\MethodCall $modifier): array
|
|
377
|
+
{
|
|
378
|
+
$args = $modifier->getArgs();
|
|
379
|
+
|
|
380
|
+
switch ($modifier->name->toString()) {
|
|
381
|
+
case 'nullable':
|
|
382
|
+
$column['nullable'] = ! isset($args[0]) || $this->literal($args[0]->value) !== false;
|
|
383
|
+
break;
|
|
384
|
+
|
|
385
|
+
case 'default':
|
|
386
|
+
if (isset($args[0]) && $this->isLiteral($args[0]->value)) {
|
|
387
|
+
$column['default'] = ['value' => $this->literal($args[0]->value)];
|
|
388
|
+
} else {
|
|
389
|
+
$column['defaultExpression'] = true;
|
|
390
|
+
}
|
|
391
|
+
break;
|
|
392
|
+
|
|
393
|
+
case 'useCurrent':
|
|
394
|
+
$column['defaultExpression'] = true;
|
|
395
|
+
break;
|
|
396
|
+
|
|
397
|
+
case 'autoIncrement':
|
|
398
|
+
$column['autoIncrement'] = true;
|
|
399
|
+
break;
|
|
400
|
+
|
|
401
|
+
case 'constrained':
|
|
402
|
+
$on = isset($args[0]) ? $this->literal($args[0]->value) : null;
|
|
403
|
+
$column['references'] = is_string($on) ? $on : preg_replace('/_id$/', '', (string) $column['name']).'s';
|
|
404
|
+
break;
|
|
405
|
+
|
|
406
|
+
case 'on':
|
|
407
|
+
$on = isset($args[0]) ? $this->literal($args[0]->value) : null;
|
|
408
|
+
$column['references'] = is_string($on) ? $on : null;
|
|
409
|
+
break;
|
|
410
|
+
|
|
411
|
+
case 'change':
|
|
412
|
+
$column['change'] = true;
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return $column;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* A column as `Migration::addColumns()` and `$table->addColumn()` give it:
|
|
421
|
+
* its type, then its options.
|
|
422
|
+
*/
|
|
423
|
+
protected function addColumn(string $table, string $name, Node\Expr\Array_ $definition): void
|
|
424
|
+
{
|
|
425
|
+
$column = ['name' => $name, 'type' => null, 'nullable' => false, 'autoIncrement' => false];
|
|
426
|
+
|
|
427
|
+
foreach ($definition->items as $item) {
|
|
428
|
+
$value = $this->literal($item?->value);
|
|
429
|
+
$option = $this->literal($item?->key);
|
|
430
|
+
|
|
431
|
+
if ($item && $item->key === null && $column['type'] === null && is_string($value)) {
|
|
432
|
+
$column['type'] = self::COLUMN_TYPES[$value] ?? null;
|
|
433
|
+
} elseif ($option === 'nullable') {
|
|
434
|
+
$column['nullable'] = (bool) $value;
|
|
435
|
+
} elseif ($option === 'default') {
|
|
436
|
+
if ($this->isLiteral($item->value)) {
|
|
437
|
+
$column['default'] = ['value' => $value];
|
|
438
|
+
} else {
|
|
439
|
+
$column['defaultExpression'] = true;
|
|
440
|
+
}
|
|
441
|
+
} elseif ($option === 'autoIncrement') {
|
|
442
|
+
$column['autoIncrement'] = (bool) $value;
|
|
443
|
+
} elseif ($option === 'allowed' && $item->value instanceof Node\Expr\Array_) {
|
|
444
|
+
$column['values'] = array_values(array_filter(array_map(fn ($allowed) => $this->literal($allowed?->value), $item->value->items), 'is_string'));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if ($column['type'] !== null) {
|
|
449
|
+
$this->define($table, $column);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Add the column, or, given change(), replace the one of that name. Since
|
|
455
|
+
* Laravel 11, a changed column keeps nothing it does not restate. Its
|
|
456
|
+
* foreign key is a separate constraint, so that stays.
|
|
457
|
+
*/
|
|
458
|
+
protected function define(string $table, array $column): void
|
|
459
|
+
{
|
|
460
|
+
$existing = $this->tables[$table][$column['name']] ?? null;
|
|
461
|
+
|
|
462
|
+
unset($column['change']);
|
|
463
|
+
|
|
464
|
+
$column += ['nullable' => false, 'autoIncrement' => false, 'references' => $existing['references'] ?? null];
|
|
465
|
+
|
|
466
|
+
if ($column['references'] === null && $existing) {
|
|
467
|
+
$column['references'] = $existing['references'] ?? null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
$this->tables[$table][$column['name']] = $column;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
protected function renameTable(string $from, mixed $to): void
|
|
474
|
+
{
|
|
475
|
+
if (is_string($to) && isset($this->tables[$from])) {
|
|
476
|
+
$this->tables[$to] = $this->tables[$from];
|
|
477
|
+
unset($this->tables[$from]);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
protected function renameColumn(string $table, mixed $from, mixed $to): void
|
|
482
|
+
{
|
|
483
|
+
if (! is_string($from) || ! is_string($to) || ! isset($this->tables[$table][$from])) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
$renamed = [];
|
|
488
|
+
|
|
489
|
+
foreach ($this->tables[$table] as $name => $column) {
|
|
490
|
+
$renamed[$name === $from ? $to : $name] = $name === $from ? ['name' => $to] + $column : $column;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
$this->tables[$table] = $renamed;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* The column names a call was given: one, or a list.
|
|
498
|
+
*
|
|
499
|
+
* @return string[]
|
|
500
|
+
*/
|
|
501
|
+
protected function names(?Node\Expr $arg): array
|
|
502
|
+
{
|
|
503
|
+
if ($arg instanceof Node\Expr\Array_) {
|
|
504
|
+
return array_values(array_filter(array_map(fn ($item) => $this->literal($item?->value), $arg->items), 'is_string'));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
$name = $this->literal($arg);
|
|
508
|
+
|
|
509
|
+
return is_string($name) ? [$name] : [];
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* @param Node\Expr\MethodCall[] $modifiers
|
|
514
|
+
*/
|
|
515
|
+
protected function modifierArgument(array $modifiers, string $method): mixed
|
|
516
|
+
{
|
|
517
|
+
foreach ($modifiers as $modifier) {
|
|
518
|
+
if ($modifier->name->toString() === $method && isset($modifier->getArgs()[0])) {
|
|
519
|
+
return $this->literal($modifier->getArgs()[0]->value);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Core models with a factory, by table.
|
|
528
|
+
*/
|
|
529
|
+
public const CORE_MODELS = [
|
|
530
|
+
'users' => 'Flarum\User\User',
|
|
531
|
+
'discussions' => 'Flarum\Discussion\Discussion',
|
|
532
|
+
'posts' => 'Flarum\Post\Post',
|
|
533
|
+
'groups' => 'Flarum\Group\Group',
|
|
534
|
+
'access_tokens' => 'Flarum\Http\AccessToken',
|
|
535
|
+
'api_keys' => 'Flarum\Api\ApiKey',
|
|
536
|
+
'tags' => 'Flarum\Tags\Tag',
|
|
537
|
+
'flags' => 'Flarum\Flags\Flag',
|
|
538
|
+
];
|
|
539
|
+
|
|
540
|
+
protected const HAS_FACTORY = 'Illuminate\Database\Eloquent\Factories\HasFactory';
|
|
541
|
+
|
|
542
|
+
protected const MODEL_BASES = ['Flarum\Database\AbstractModel', 'Illuminate\Database\Eloquent\Model'];
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* The models among an extension's classes: those that extend a model base
|
|
546
|
+
* class directly or through the extension's own classes. A subclass of a
|
|
547
|
+
* core model, such as a post type, shares that model's table and factory.
|
|
548
|
+
*
|
|
549
|
+
* @param array<string, string> $files Each file's code, by path.
|
|
550
|
+
*/
|
|
551
|
+
public function models(array $files): array
|
|
552
|
+
{
|
|
553
|
+
$classes = [];
|
|
554
|
+
|
|
555
|
+
foreach ($files as $file => $code) {
|
|
556
|
+
foreach ($this->classesIn($code) as $class) {
|
|
557
|
+
$classes[$class['class']] = $class + ['file' => $file];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
$models = [];
|
|
562
|
+
|
|
563
|
+
foreach ($classes as $name => $class) {
|
|
564
|
+
$ancestors = [];
|
|
565
|
+
$parent = $class['parent'];
|
|
566
|
+
|
|
567
|
+
while ($parent !== null && isset($classes[$parent]) && ! in_array($parent, $ancestors)) {
|
|
568
|
+
$ancestors[] = $parent;
|
|
569
|
+
$parent = $classes[$parent]['parent'];
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if ($class['abstract'] || ! in_array($parent, self::MODEL_BASES)) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
$lineage = [$class, ...array_map(fn (string $ancestor) => $classes[$ancestor], $ancestors)];
|
|
577
|
+
|
|
578
|
+
$models[] = [
|
|
579
|
+
'file' => $class['file'],
|
|
580
|
+
'class' => $name,
|
|
581
|
+
'table' => array_values(array_filter(array_column($lineage, 'table')))[0] ?? null,
|
|
582
|
+
'hasFactory' => in_array(true, array_column($lineage, 'hasFactory'), true),
|
|
583
|
+
];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
usort($models, fn (array $a, array $b) => strcmp($a['file'], $b['file']));
|
|
587
|
+
|
|
588
|
+
return ['models' => $models, 'core' => self::CORE_MODELS];
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* The classes a file declares, with names resolved.
|
|
593
|
+
*/
|
|
594
|
+
protected function classesIn(string $code): array
|
|
595
|
+
{
|
|
596
|
+
$ast = (new ParserFactory())->createForHostVersion()->parse($code) ?? [];
|
|
597
|
+
|
|
598
|
+
$traverser = new NodeTraverser();
|
|
599
|
+
$traverser->addVisitor(new NodeVisitor\NameResolver());
|
|
600
|
+
$ast = $traverser->traverse($ast);
|
|
601
|
+
|
|
602
|
+
$classes = [];
|
|
603
|
+
|
|
604
|
+
foreach ((new \PhpParser\NodeFinder())->findInstanceOf($ast, Node\Stmt\Class_::class) as $class) {
|
|
605
|
+
if (! $class->namespacedName) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
$table = null;
|
|
610
|
+
$hasFactory = false;
|
|
611
|
+
|
|
612
|
+
foreach ($class->stmts as $statement) {
|
|
613
|
+
if ($statement instanceof Node\Stmt\Property) {
|
|
614
|
+
foreach ($statement->props as $property) {
|
|
615
|
+
if ($property->name->toString() === 'table' && is_string($this->literal($property->default))) {
|
|
616
|
+
$table = $this->literal($property->default);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if ($statement instanceof Node\Stmt\TraitUse) {
|
|
622
|
+
foreach ($statement->traits as $trait) {
|
|
623
|
+
$hasFactory = $hasFactory || $trait->toString() === self::HAS_FACTORY;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
$classes[] = [
|
|
629
|
+
'class' => $class->namespacedName->toString(),
|
|
630
|
+
'parent' => $class->extends?->toString(),
|
|
631
|
+
'abstract' => $class->isAbstract(),
|
|
632
|
+
'table' => $table,
|
|
633
|
+
'hasFactory' => $hasFactory,
|
|
634
|
+
];
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return $classes;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* A factory for the model the file declares, from its table's columns,
|
|
642
|
+
* and the model with the HasFactory trait.
|
|
643
|
+
*
|
|
644
|
+
* @param array{columns: ?array, models: array<string, string>} $data
|
|
645
|
+
* The model's table's columns (null if the migrations say nothing of
|
|
646
|
+
* it), and the models with a factory, by table.
|
|
647
|
+
*/
|
|
648
|
+
public function factory(string $file, string $code, array $data): array
|
|
649
|
+
{
|
|
650
|
+
$ast = (new ParserFactory())->createForHostVersion()->parse($code) ?? [];
|
|
651
|
+
|
|
652
|
+
$traverser = new NodeTraverser();
|
|
653
|
+
$traverser->addVisitor(new NodeVisitor\NameResolver(null, ['replaceNodes' => false]));
|
|
654
|
+
$ast = $traverser->traverse($ast);
|
|
655
|
+
|
|
656
|
+
$finder = new \PhpParser\NodeFinder();
|
|
657
|
+
$namespace = $finder->findFirstInstanceOf($ast, Node\Stmt\Namespace_::class);
|
|
658
|
+
$class = $finder->findFirst($ast, fn (Node $node) => $node instanceof Node\Stmt\Class_ && ! $node->isAbstract() && $node->name);
|
|
659
|
+
|
|
660
|
+
if (! $namespace || ! $class) {
|
|
661
|
+
return ['code' => $code, 'factory' => null];
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
$model = $class->namespacedName->toString();
|
|
665
|
+
$table = array_search($model, $data['models'] ?? [], true);
|
|
666
|
+
|
|
667
|
+
return [
|
|
668
|
+
'code' => $this->withHasFactory($code, $namespace, $class),
|
|
669
|
+
'factory' => $this->factoryCode($namespace, $class->name->toString(), $data['columns'] ?? null, $data['models'] ?? [], is_string($table) ? $table : null),
|
|
670
|
+
];
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
protected function factoryCode(Node\Stmt\Namespace_ $namespace, string $model, ?array $columns, array $models, ?string $table): string
|
|
674
|
+
{
|
|
675
|
+
$uses = ['Illuminate\Database\Eloquent\Factories\Factory' => 'Factory'];
|
|
676
|
+
$lines = [];
|
|
677
|
+
|
|
678
|
+
foreach ($columns ?? [] as $column) {
|
|
679
|
+
$value = $this->default($column, $table, $models, $namespace->name->toString(), $uses);
|
|
680
|
+
|
|
681
|
+
if ($value === 'SKIP') {
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
$lines[] = str_starts_with($value, '//')
|
|
686
|
+
? ' '.$value
|
|
687
|
+
: ' '.$this->export($column['name']).' => '.$value.',';
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if ($columns === null) {
|
|
691
|
+
$lines = [' // @TODO: give each column a default, so tests only need to set the ones they care about.'];
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
ksort($uses, SORT_STRING | SORT_FLAG_CASE);
|
|
695
|
+
|
|
696
|
+
$header = '';
|
|
697
|
+
|
|
698
|
+
foreach ($namespace->getComments() as $comment) {
|
|
699
|
+
$header .= $comment->getText()."\n\n";
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return "<?php\n\n".$header.'namespace '.$namespace->name->toString().";\n\n"
|
|
703
|
+
.implode('', array_map(fn (string $use) => "use $use;\n", array_keys($uses)))
|
|
704
|
+
."\nclass {$model}Factory extends Factory\n{\n"
|
|
705
|
+
." public function definition(): array\n {\n return [\n"
|
|
706
|
+
.implode("\n", $lines).($lines ? "\n" : '')
|
|
707
|
+
." ];\n }\n}\n";
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* The PHP for a column's default in the factory, a // @TODO line when it
|
|
712
|
+
* needs one the upgrade can't give, or SKIP when the database gives it.
|
|
713
|
+
*/
|
|
714
|
+
protected function default(array $column, ?string $table, array $models, string $namespace, array &$uses): string
|
|
715
|
+
{
|
|
716
|
+
$name = $column['name'];
|
|
717
|
+
|
|
718
|
+
if ($column['autoIncrement']) {
|
|
719
|
+
return 'SKIP';
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if ($name === 'created_at') {
|
|
723
|
+
$uses['Carbon\Carbon'] = 'Carbon';
|
|
724
|
+
|
|
725
|
+
return 'Carbon::now()';
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if ($column['references'] ?? null) {
|
|
729
|
+
if ($column['nullable']) {
|
|
730
|
+
return 'null';
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
$related = $models[$column['references']] ?? null;
|
|
734
|
+
|
|
735
|
+
// A model pointing at its own table would make one of itself forever.
|
|
736
|
+
if (! $related || $column['references'] === $table) {
|
|
737
|
+
return "// @TODO: give $name a default, such as a factory for the model it points at, or set it in each test.";
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return $this->classReference($related, $namespace, $uses).'::factory()';
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (array_key_exists('default', $column)) {
|
|
744
|
+
return $this->export($column['default']['value']);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if ($column['defaultExpression'] ?? false) {
|
|
748
|
+
return 'SKIP';
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if ($column['nullable']) {
|
|
752
|
+
return 'null';
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
switch ($column['type']) {
|
|
756
|
+
case 'int':
|
|
757
|
+
case 'number':
|
|
758
|
+
return '0';
|
|
759
|
+
case 'bool':
|
|
760
|
+
return 'false';
|
|
761
|
+
case 'datetime':
|
|
762
|
+
$uses['Carbon\Carbon'] = 'Carbon';
|
|
763
|
+
|
|
764
|
+
return 'Carbon::now()';
|
|
765
|
+
case 'json':
|
|
766
|
+
return '[]';
|
|
767
|
+
case 'enum':
|
|
768
|
+
return isset($column['values'][0]) ? $this->export($column['values'][0]) : "// @TODO: give $name a default, or set it in each test.";
|
|
769
|
+
case 'uuid':
|
|
770
|
+
return '$this->faker->uuid';
|
|
771
|
+
case 'ip':
|
|
772
|
+
return '$this->faker->ipv4';
|
|
773
|
+
case 'binary':
|
|
774
|
+
return "''";
|
|
775
|
+
case 'string':
|
|
776
|
+
case 'text':
|
|
777
|
+
return match (true) {
|
|
778
|
+
$name === 'slug' => '$this->faker->slug',
|
|
779
|
+
$name === 'email' => '$this->faker->safeEmail',
|
|
780
|
+
$name === 'ip_address' => '$this->faker->ipv4',
|
|
781
|
+
$name === 'url' || str_ends_with($name, '_url') => '$this->faker->url',
|
|
782
|
+
$name === 'color' => '$this->faker->hexColor',
|
|
783
|
+
$column['type'] === 'text' => '$this->faker->sentence',
|
|
784
|
+
default => '$this->faker->word',
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return "// @TODO: give $name a default, or set it in each test.";
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* How the factory names a model class: imported, unless it is in the
|
|
793
|
+
* factory's namespace or its name is taken.
|
|
794
|
+
*/
|
|
795
|
+
protected function classReference(string $class, string $namespace, array &$uses): string
|
|
796
|
+
{
|
|
797
|
+
$short = substr($class, strrpos($class, '\\') + 1);
|
|
798
|
+
|
|
799
|
+
if (substr($class, 0, -strlen($short) - 1) === $namespace) {
|
|
800
|
+
return $short;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (isset($uses[$class]) || ! in_array($short, $uses, true)) {
|
|
804
|
+
$uses[$class] = $short;
|
|
805
|
+
|
|
806
|
+
return $short;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return '\\'.$class;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
protected function export(mixed $value): string
|
|
813
|
+
{
|
|
814
|
+
return match (true) {
|
|
815
|
+
$value === null => 'null',
|
|
816
|
+
is_bool($value) => $value ? 'true' : 'false',
|
|
817
|
+
is_int($value), is_float($value) => (string) $value,
|
|
818
|
+
default => "'".addcslashes((string) $value, "\\'")."'",
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* The model's code with the HasFactory trait, and its import, added where
|
|
824
|
+
* they belong. Edited as text, from the end, so the rest stays as it is.
|
|
825
|
+
*/
|
|
826
|
+
protected function withHasFactory(string $code, Node\Stmt\Namespace_ $namespace, Node\Stmt\Class_ $class): string
|
|
827
|
+
{
|
|
828
|
+
foreach ($class->stmts as $statement) {
|
|
829
|
+
if ($statement instanceof Node\Stmt\TraitUse) {
|
|
830
|
+
foreach ($statement->traits as $trait) {
|
|
831
|
+
if ($trait->getAttribute('resolvedName')?->toString() === self::HAS_FACTORY) {
|
|
832
|
+
return $code;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
$edits = [];
|
|
839
|
+
|
|
840
|
+
// The trait: with the class's other traits, else above everything else.
|
|
841
|
+
$first = $class->stmts[0] ?? null;
|
|
842
|
+
$trait = null;
|
|
843
|
+
|
|
844
|
+
foreach ($class->stmts as $statement) {
|
|
845
|
+
if ($statement instanceof Node\Stmt\TraitUse) {
|
|
846
|
+
$trait = $statement;
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if ($trait || $first) {
|
|
852
|
+
$at = $this->startOf($trait ?? $first);
|
|
853
|
+
$indent = $this->indentAt($code, $at);
|
|
854
|
+
$edits[$at] = 'use HasFactory;'."\n".($trait ? '' : "\n").$indent;
|
|
855
|
+
} else {
|
|
856
|
+
// An empty body: everything between its braces becomes the trait.
|
|
857
|
+
$close = $class->getEndFilePos();
|
|
858
|
+
$open = strrpos(substr($code, 0, $close), '{');
|
|
859
|
+
$indent = $this->indentAt($code, $class->getStartFilePos());
|
|
860
|
+
$edits[$open] = "{\n$indent use HasFactory;\n$indent}";
|
|
861
|
+
$code = substr($code, 0, $open).substr($code, $close + 1);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// The import: in order among the others, unless it is there already.
|
|
865
|
+
$imports = array_values(array_filter($namespace->stmts, fn (Node $node) => $node instanceof Node\Stmt\Use_ && $node->type === Node\Stmt\Use_::TYPE_NORMAL));
|
|
866
|
+
$imported = array_map(fn (Node\Stmt\Use_ $use) => $use->uses[0]->name->toString(), $imports);
|
|
867
|
+
|
|
868
|
+
if (! in_array(self::HAS_FACTORY, $imported)) {
|
|
869
|
+
$after = null;
|
|
870
|
+
|
|
871
|
+
foreach ($imports as $import) {
|
|
872
|
+
if (strcasecmp($import->uses[0]->name->toString(), self::HAS_FACTORY) > 0) {
|
|
873
|
+
$edits[$this->startOf($import)] = 'use '.self::HAS_FACTORY.";\n";
|
|
874
|
+
$after = false;
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
$after = $import;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if ($after) {
|
|
882
|
+
$edits[$after->getEndFilePos() + 1] = "\nuse ".self::HAS_FACTORY.';';
|
|
883
|
+
} elseif ($after === null) {
|
|
884
|
+
$edits[$namespace->name->getEndFilePos() + 2] = "\n\nuse ".self::HAS_FACTORY.';';
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
krsort($edits);
|
|
889
|
+
|
|
890
|
+
foreach ($edits as $at => $text) {
|
|
891
|
+
$code = substr($code, 0, $at).$text.substr($code, $at);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return $code;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Where a statement starts, comments included.
|
|
899
|
+
*/
|
|
900
|
+
protected function startOf(Node $node): int
|
|
901
|
+
{
|
|
902
|
+
$comments = $node->getComments();
|
|
903
|
+
|
|
904
|
+
return $comments ? $comments[0]->getStartFilePos() : $node->getStartFilePos();
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
protected function indentAt(string $code, int $at): string
|
|
908
|
+
{
|
|
909
|
+
$lineStart = strrpos(substr($code, 0, $at), "\n");
|
|
910
|
+
|
|
911
|
+
return substr($code, $lineStart === false ? 0 : $lineStart + 1, $at - ($lineStart === false ? 0 : $lineStart + 1));
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
protected function isLiteral(?Node\Expr $expr): bool
|
|
915
|
+
{
|
|
916
|
+
return $expr instanceof Node\Scalar\String_
|
|
917
|
+
|| $expr instanceof Node\Scalar\Int_
|
|
918
|
+
|| $expr instanceof Node\Scalar\Float_
|
|
919
|
+
|| ($expr instanceof Node\Expr\UnaryMinus && ($expr->expr instanceof Node\Scalar\Int_ || $expr->expr instanceof Node\Scalar\Float_))
|
|
920
|
+
|| ($expr instanceof Node\Expr\ConstFetch && in_array($expr->name->toLowerString(), ['true', 'false', 'null']));
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* The value of a literal: a string, number, boolean or null. Anything else
|
|
925
|
+
* is null.
|
|
926
|
+
*/
|
|
927
|
+
protected function literal(?Node\Expr $expr): mixed
|
|
928
|
+
{
|
|
929
|
+
if (! $this->isLiteral($expr)) {
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if ($expr instanceof Node\Expr\UnaryMinus) {
|
|
934
|
+
return -$expr->expr->value;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if ($expr instanceof Node\Expr\ConstFetch) {
|
|
938
|
+
return ['true' => true, 'false' => false, 'null' => null][$expr->name->toLowerString()];
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
return $expr->value;
|
|
942
|
+
}
|
|
943
|
+
}
|