Repository navigation
Expand file tree
/
Copy pathScaffoldCommands.php
More file actions
171 lines (161 loc) · 9.77 KB
/
Copy pathScaffoldCommands.php
File metadata and controls
171 lines (161 loc) · 9.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
<?php
declare(strict_types=1);
namespace Loom\Console\Commands;
use Loom\Config;
use Loom\Console\Console;
use Loom\Console\Definition;
use Loom\Console\Input;
use Loom\Console\InputError;
use Loom\Console\Output;
use Loom\GrpcGenerator;
use Loom\ProtoTools;
/**
* 代码生成命令:make:command、make:timer、make:proto、proto:check、make:grpc。由 Application 在构建时登记。
*
* @example ScaffoldCommands::register($console);
*/
final class ScaffoldCommands
{
private const RESERVED = 'abstract and array as bool break callable case catch class clone const continue declare default die do echo else elseif empty enddeclare endfor endforeach endif endswitch endwhile enum eval exit extends false final finally float fn for foreach function global goto if implements include include_once instanceof insteadof int interface isset iterable list match mixed namespace never new null object or parent print private protected public readonly require require_once resource return self static string switch throw trait true try unset use var void while xor yield';
public static function register(Console $console): void
{
$console->command('make:command {name : PascalCase 命令类名} {--command= : 命令签名}', static function (Input $input, Output $output, Config $config) use ($console): int {
$name = $input->argument('name');
if (!preg_match('/^[A-Z][A-Za-z0-9]{0,79}$/D', $name)) {
throw new InputError('name 必须是单个 PascalCase 类名,不接受路径或命名空间');
}
if (in_array(strtolower($name), explode(' ', self::RESERVED), true)) {
throw new InputError('name 不能是 PHP 保留字');
}
$signature = $input->option('command') ?? ('app:' . strtolower($name));
try {
$definition = new Definition($signature, '');
} catch (\LogicException $error) {
throw new InputError($error->getMessage());
}
if ($console->has($definition->name)) {
throw new InputError('命令名已存在:' . $definition->name);
}
$directory = $config->root() . '/app/Console/Commands';
if (!is_dir($directory) && !mkdir($directory, 0755, true)) {
throw new \RuntimeException('无法创建命令目录');
}
$path = $directory . '/' . $name . '.php';
if (file_exists($path)) {
throw new InputError('命令文件已存在,未覆盖:' . $path);
}
$source = "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Console\\Commands;\n\nuse Loom\\Console\\Command;\n\n#[\\Cli\\Annotation\\Command]\nfinal class " . $name . " extends Command\n{\n protected string \$signature = " . var_export($signature, true) . ";\n protected string \$description = '请填写命令说明';\n\n public function handle(): int\n {\n \$this->info('命令已就绪,请在 handle 方法中实现业务逻辑。');\n return 0;\n }\n}\n";
// 独占创建,并发执行时不会覆盖已有文件。
$handle = fopen($path, 'x');
if ($handle === false) {
throw new \RuntimeException('无法独占创建命令文件');
}
try {
if (fwrite($handle, $source) !== strlen($source)) {
throw new \RuntimeException('命令文件写入不完整');
}
} finally {
fclose($handle);
}
$output->info('已创建:' . $path);
return 0;
}, '生成自动注册的 PHP 命令类,不覆盖已有文件');
$console->command('make:timer {name : PascalCase 定时器类名} {--seconds= : 固定周期(秒,至少 1)} {--cron= : 五段 Cron 表达式} {--timezone=UTC : Cron IANA 时区}', static function (Input $input, Output $output, Config $config): int {
$name = $input->argument('name');
if (!preg_match('/^[A-Z][A-Za-z0-9]{0,79}$/D', $name)) {
throw new InputError('name 必须是单个 PascalCase 类名,不接受路径或命名空间');
}
if (in_array(strtolower($name), explode(' ', self::RESERVED), true)) {
throw new InputError('name 不能是 PHP 保留字');
}
$cron = trim((string)$input->option('cron'));
$secondsRaw = $input->option('seconds');
$hasSeconds = $secondsRaw !== null && $secondsRaw !== '';
$seconds = $hasSeconds ? (int)$secondsRaw : 0;
if ($cron !== '' && $hasSeconds) {
throw new InputError('不能同时指定 --seconds 与 --cron');
}
if ($cron === '' && !$hasSeconds) {
$seconds = 60;
}
if ($cron === '' && $seconds < 1) {
throw new InputError('seconds 必须至少为 1');
}
if ($cron !== '') {
try {
new \Loom\Cron($cron, (string)$input->option('timezone'));
} catch (\Loom\HttpError $error) {
throw new InputError($error->getMessage());
}
}
$directory = $config->root() . '/app/Console/Timers';
if (!is_dir($directory) && !mkdir($directory, 0755, true)) {
throw new \RuntimeException('无法创建定时器目录');
}
$path = $directory . '/' . $name . '.php';
if (file_exists($path)) {
throw new InputError('定时器文件已存在,未覆盖:' . $path);
}
$attribute = $cron !== ''
? "#[Timer(0, " . var_export($cron, true) . ', ' . var_export((string)$input->option('timezone'), true) . ')]'
: "#[Timer(" . $seconds . ")]";
$source = "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Console\\Timers;\n\nuse Loom\\Timer;\n\n" . $attribute . "\nfinal class " . $name . "\n{\n public function __invoke(): void\n {\n // 定时任务逻辑\n }\n}\n";
$handle = fopen($path, 'x');
if ($handle === false) {
throw new \RuntimeException('无法独占创建定时器文件');
}
try {
if (fwrite($handle, $source) !== strlen($source)) {
throw new \RuntimeException('定时器文件写入不完整');
}
} finally {
fclose($handle);
}
$output->info('已创建:' . $path);
return 0;
}, '生成带 #[Timer] 注解的定时器类,不覆盖已有文件');
$console->command('make:websocket {name : PascalCase 处理器类名}', static function (Input $input, Output $output, Config $config): int {
$name = $input->argument('name');
if (!preg_match('/^[A-Z][A-Za-z0-9]{0,79}$/D', $name)) {
throw new InputError('name 必须是单个 PascalCase 类名,不接受路径或命名空间');
}
if (in_array(strtolower($name), explode(' ', self::RESERVED), true)) {
throw new InputError('name 不能是 PHP 保留字');
}
$directory = $config->root() . '/app/WebSocket/Handlers';
if (!is_dir($directory) && !mkdir($directory, 0755, true)) {
throw new \RuntimeException('无法创建 WebSocket 处理器目录');
}
$path = $directory . '/' . $name . '.php';
if (file_exists($path)) {
throw new InputError('处理器文件已存在,未覆盖:' . $path);
}
$source = "<?php\ndeclare(strict_types=1);\n\nnamespace App\\WebSocket\\Handlers;\n\n#[\\Container\\Annotation\\Scoped]\nfinal class " . $name . "\n{\n #[\\Loom\\Attributes\\WebSocket('/api/example/socket', middleware: [])]\n public function connect(): void {}\n\n public function onOpen(\\Loom\\Request \$request, \\Net\\Websocket\\Conn \$connection): void {}\n\n #[\\Loom\\Attributes\\WebSocketMessage('ping')]\n public function ping(array \$message, \\Net\\Websocket\\Conn \$connection): void\n {\n \$connection->writeText(json_encode(['type' => 'pong'], JSON_THROW_ON_ERROR));\n }\n}\n";
$handle = fopen($path, 'x');
if ($handle === false) {
throw new \RuntimeException('无法独占创建处理器文件');
}
try {
if (fwrite($handle, $source) !== strlen($source)) {
throw new \RuntimeException('处理器文件写入不完整');
}
} finally {
fclose($handle);
}
$output->info('已创建:' . $path);
return 0;
}, '生成带 #[WebSocket] 注解的处理器骨架');
$console->command('make:proto {name : PascalCase 名称,例如 User} {--package=app : Protobuf 包名}', static function (Input $input, ProtoTools $tools): int {
$tools->create($input->argument('name'), $input->option('package'));
return 0;
}, '创建 proto3 协议模板,不覆盖已有文件');
$console->command('proto:check {file? : 可选的单个 proto 文件}', static function (Input $input, ProtoTools $tools): int {
$tools->check($input->argument('file'));
return 0;
}, '检查协议语法、imports、命名冲突和不支持的 RPC');
$console->command('make:grpc {file : proto 文件} {--dry-run : 预览文件变更,不写入}', static function (Input $input, GrpcGenerator $generator): int {
$generator->generate($input->argument('file'), $input->option('dry-run'));
return 0;
}, '生成 gRPC 基类与注册清单,保留手写业务文件并提示待实现方法');
}
}