create-prisma-php-app 3.5.4 → 3.6.1
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/dist/.htaccess +54 -41
- package/dist/bootstrap.php +12 -7
- package/dist/index.js +41 -30
- package/dist/settings/auto-swagger-docs.ts +195 -94
- package/dist/settings/bs-config.ts +53 -58
- package/dist/settings/project-name.ts +2 -0
- package/dist/settings/restart-mcp.ts +58 -0
- package/dist/settings/restart-websocket.ts +44 -45
- package/dist/settings/utils.ts +240 -0
- package/dist/src/Lib/MCP/WeatherTools.php +104 -0
- package/dist/src/Lib/MCP/mcp-server.php +80 -0
- package/dist/src/Lib/Middleware/AuthMiddleware.php +6 -3
- package/dist/src/Lib/Middleware/CorsMiddleware.php +145 -0
- package/dist/src/Lib/Websocket/websocket-server.php +105 -14
- package/package.json +1 -1
- package/dist/settings/restart-websocket.bat +0 -28
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Lib\Middleware;
|
|
6
|
+
|
|
7
|
+
final class CorsMiddleware
|
|
8
|
+
{
|
|
9
|
+
/** Entry point */
|
|
10
|
+
public static function handle(?array $overrides = null): void
|
|
11
|
+
{
|
|
12
|
+
// Not a CORS request
|
|
13
|
+
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
|
14
|
+
if ($origin === '') {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Resolve config (env → overrides)
|
|
19
|
+
$cfg = self::buildConfig($overrides);
|
|
20
|
+
|
|
21
|
+
// Not allowed? Do nothing (browser will block)
|
|
22
|
+
if (!self::isAllowedOrigin($origin, $cfg['allowedOrigins'])) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Compute which value to send for Access-Control-Allow-Origin
|
|
27
|
+
// If credentials are disabled and '*' is in list, we can send '*'
|
|
28
|
+
$sendWildcard = (!$cfg['allowCredentials'] && self::listHasWildcard($cfg['allowedOrigins']));
|
|
29
|
+
$allowOriginValue = $sendWildcard ? '*' : self::normalize($origin);
|
|
30
|
+
|
|
31
|
+
// Vary for caches
|
|
32
|
+
header('Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers');
|
|
33
|
+
|
|
34
|
+
header('Access-Control-Allow-Origin: ' . $allowOriginValue);
|
|
35
|
+
if ($cfg['allowCredentials']) {
|
|
36
|
+
header('Access-Control-Allow-Credentials: true');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
40
|
+
// Preflight response
|
|
41
|
+
$requestedHeaders = $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'] ?? '';
|
|
42
|
+
$allowedHeaders = $cfg['allowedHeaders'] !== ''
|
|
43
|
+
? $cfg['allowedHeaders']
|
|
44
|
+
: ($requestedHeaders ?: 'Content-Type, Authorization, X-Requested-With');
|
|
45
|
+
|
|
46
|
+
header('Access-Control-Allow-Methods: ' . $cfg['allowedMethods']);
|
|
47
|
+
header('Access-Control-Allow-Headers: ' . $allowedHeaders);
|
|
48
|
+
if ($cfg['maxAge'] > 0) {
|
|
49
|
+
header('Access-Control-Max-Age: ' . (string) $cfg['maxAge']);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Optional: Private Network Access preflights (Chrome)
|
|
53
|
+
if (!empty($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK'])) {
|
|
54
|
+
header('Access-Control-Allow-Private-Network: true');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
http_response_code(204);
|
|
58
|
+
header('Content-Length: 0');
|
|
59
|
+
exit;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Simple/actual request
|
|
63
|
+
if ($cfg['exposeHeaders'] !== '') {
|
|
64
|
+
header('Access-Control-Expose-Headers: ' . $cfg['exposeHeaders']);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Read env + normalize + apply overrides */
|
|
69
|
+
private static function buildConfig(?array $overrides): array
|
|
70
|
+
{
|
|
71
|
+
$allowed = self::parseList($_ENV['CORS_ALLOWED_ORIGINS'] ?? '');
|
|
72
|
+
$cfg = [
|
|
73
|
+
'allowedOrigins' => $allowed,
|
|
74
|
+
'allowCredentials' => filter_var($_ENV['CORS_ALLOW_CREDENTIALS'] ?? 'false', FILTER_VALIDATE_BOOLEAN),
|
|
75
|
+
'allowedMethods' => $_ENV['CORS_ALLOWED_METHODS'] ?? 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
|
|
76
|
+
'allowedHeaders' => trim($_ENV['CORS_ALLOWED_HEADERS'] ?? ''),
|
|
77
|
+
'exposeHeaders' => trim($_ENV['CORS_EXPOSE_HEADERS'] ?? ''),
|
|
78
|
+
'maxAge' => (int)($_ENV['CORS_MAX_AGE'] ?? 86400),
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
if (is_array($overrides)) {
|
|
82
|
+
foreach ($overrides as $k => $v) {
|
|
83
|
+
if (array_key_exists($k, $cfg)) {
|
|
84
|
+
$cfg[$k] = $v;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Normalize patterns
|
|
90
|
+
$cfg['allowedOrigins'] = array_map([self::class, 'normalize'], $cfg['allowedOrigins']);
|
|
91
|
+
return $cfg;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** CSV or JSON array → array<string> */
|
|
95
|
+
private static function parseList(string $raw): array
|
|
96
|
+
{
|
|
97
|
+
$raw = trim($raw);
|
|
98
|
+
if ($raw === '') return [];
|
|
99
|
+
|
|
100
|
+
if ($raw[0] === '[') {
|
|
101
|
+
$arr = json_decode($raw, true);
|
|
102
|
+
if (is_array($arr)) {
|
|
103
|
+
return array_values(array_filter(array_map('strval', $arr), 'strlen'));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return array_values(array_filter(array_map('trim', explode(',', $raw)), 'strlen'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private static function normalize(string $origin): string
|
|
110
|
+
{
|
|
111
|
+
return rtrim($origin, '/');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private static function isAllowedOrigin(string $origin, array $list): bool
|
|
115
|
+
{
|
|
116
|
+
$o = self::normalize($origin);
|
|
117
|
+
|
|
118
|
+
foreach ($list as $pattern) {
|
|
119
|
+
$p = self::normalize($pattern);
|
|
120
|
+
|
|
121
|
+
// literal "*"
|
|
122
|
+
if ($p === '*') return true;
|
|
123
|
+
|
|
124
|
+
// allow literal "null" for file:// or sandboxed if explicitly listed
|
|
125
|
+
if ($o === 'null' && strtolower($p) === 'null') return true;
|
|
126
|
+
|
|
127
|
+
// wildcard like https://*.example.com
|
|
128
|
+
if (strpos($p, '*') !== false) {
|
|
129
|
+
$regex = '/^' . str_replace('\*', '[^.]+', preg_quote($p, '/')) . '$/i';
|
|
130
|
+
if (preg_match($regex, $o)) return true;
|
|
131
|
+
} else {
|
|
132
|
+
if (strcasecmp($p, $o) === 0) return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private static function listHasWildcard(array $list): bool
|
|
139
|
+
{
|
|
140
|
+
foreach ($list as $p) {
|
|
141
|
+
if (trim($p) === '*') return true;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -2,26 +2,117 @@
|
|
|
2
2
|
|
|
3
3
|
declare(strict_types=1);
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
require_once __DIR__ . '/../../../settings/paths.php';
|
|
5
|
+
namespace Lib\Websocket;
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
$root = dirname(__DIR__, 3);
|
|
8
|
+
require $root . '/vendor/autoload.php';
|
|
9
|
+
require_once $root . '/settings/paths.php';
|
|
11
10
|
|
|
11
|
+
use Dotenv\Dotenv;
|
|
12
12
|
use Ratchet\Server\IoServer;
|
|
13
13
|
use Ratchet\Http\HttpServer;
|
|
14
14
|
use Ratchet\WebSocket\WsServer;
|
|
15
15
|
use Lib\Websocket\ConnectionManager;
|
|
16
|
+
use Throwable;
|
|
17
|
+
|
|
18
|
+
// ── Load .env (optional) and timezone ─────────────────────────────────────────
|
|
19
|
+
if (file_exists(DOCUMENT_PATH . '/.env')) {
|
|
20
|
+
Dotenv::createImmutable(DOCUMENT_PATH)->safeLoad();
|
|
21
|
+
}
|
|
22
|
+
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'UTC');
|
|
23
|
+
|
|
24
|
+
// ── Tiny argv parser: allows --host=0.0.0.0 --port=8080 ──────────────────────
|
|
25
|
+
$cli = [];
|
|
26
|
+
foreach ($argv ?? [] as $arg) {
|
|
27
|
+
if (preg_match('/^--([^=]+)=(.*)$/', $arg, $m)) {
|
|
28
|
+
$cli[$m[1]] = $m[2];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Resolve settings (env → cli defaults) ────────────────────────────────────
|
|
33
|
+
$appName = $_ENV['WS_NAME'] ?? 'prisma-php-ws';
|
|
34
|
+
$appVer = $_ENV['WS_VERSION'] ?? '0.0.1';
|
|
35
|
+
$host = $cli['host'] ?? ($_ENV['WS_HOST'] ?? '127.0.0.1');
|
|
36
|
+
$port = (int)($cli['port'] ?? ($_ENV['WS_PORT'] ?? 8080));
|
|
37
|
+
$verbose = filter_var($cli['verbose'] ?? ($_ENV['WS_VERBOSE'] ?? 'true'), FILTER_VALIDATE_BOOLEAN);
|
|
38
|
+
|
|
39
|
+
// ── Console helpers ──────────────────────────────────────────────────────────
|
|
40
|
+
$color = static fn(string $t, string $c) => "\033[{$c}m{$t}\033[0m";
|
|
41
|
+
$info = static fn(string $t) => $color($t, '1;36');
|
|
42
|
+
$ok = static fn(string $t) => $color($t, '32');
|
|
43
|
+
$warn = static fn(string $t) => $color($t, '33');
|
|
44
|
+
$err = static fn(string $t) => $color($t, '31');
|
|
16
45
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
46
|
+
// ── Preflight: check if port is free (nice error if not) ─────────────────────
|
|
47
|
+
$probe = @stream_socket_server("tcp://{$host}:{$port}", $errno, $errstr);
|
|
48
|
+
if ($probe === false) {
|
|
49
|
+
fwrite(STDERR, $err("✖ Port {$port} on {$host} is not available: {$errstr}\n"));
|
|
50
|
+
exit(1);
|
|
51
|
+
}
|
|
52
|
+
fclose($probe);
|
|
53
|
+
|
|
54
|
+
// ── Build app ────────────────────────────────────────────────────────────────
|
|
55
|
+
$manager = new ConnectionManager(); // your app component
|
|
56
|
+
$server = IoServer::factory(
|
|
57
|
+
new HttpServer(new WsServer($manager)),
|
|
58
|
+
$port,
|
|
59
|
+
$host
|
|
24
60
|
);
|
|
25
61
|
|
|
26
|
-
|
|
27
|
-
$
|
|
62
|
+
$pid = getmypid() ?: 0;
|
|
63
|
+
$url = "ws://{$host}:{$port}";
|
|
64
|
+
$ts = date('Y-m-d H:i:s');
|
|
65
|
+
|
|
66
|
+
echo PHP_EOL;
|
|
67
|
+
echo $info("⚡ {$appName} (v{$appVer})") . PHP_EOL;
|
|
68
|
+
echo $warn("→ WebSocket server starting…") . PHP_EOL;
|
|
69
|
+
echo " Host: {$host}" . PHP_EOL;
|
|
70
|
+
echo " Port: {$port}" . PHP_EOL;
|
|
71
|
+
echo " URL: {$url}" . PHP_EOL;
|
|
72
|
+
echo " PID: {$pid}" . PHP_EOL;
|
|
73
|
+
echo " Started: {$ts}" . PHP_EOL;
|
|
74
|
+
|
|
75
|
+
// ── Graceful shutdown & periodic logs (if loop available) ────────────────────
|
|
76
|
+
$loop = property_exists($server, 'loop') ? $server->loop : null;
|
|
77
|
+
if ($loop instanceof \React\EventLoop\LoopInterface) {
|
|
78
|
+
// Periodic stats every 60s
|
|
79
|
+
$loop->addPeriodicTimer(60, function () use ($ok) {
|
|
80
|
+
$mem = function_exists('memory_get_usage') ? number_format(memory_get_usage(true) / 1048576, 2) . ' MB' : 'n/a';
|
|
81
|
+
$msg = "✓ Heartbeat — memory: {$mem}";
|
|
82
|
+
echo $ok($msg) . PHP_EOL;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Signal handlers (needs pcntl)
|
|
86
|
+
if (function_exists('pcntl_signal') && method_exists($loop, 'addSignal')) {
|
|
87
|
+
$stop = function (int $sig) use ($warn, $loop) {
|
|
88
|
+
echo PHP_EOL . $warn("⏹ Caught signal {$sig}. Shutting down…") . PHP_EOL;
|
|
89
|
+
$loop->stop();
|
|
90
|
+
};
|
|
91
|
+
$loop->addSignal(SIGINT, $stop);
|
|
92
|
+
$loop->addSignal(SIGTERM, $stop);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Run ──────────────────────────────────────────────────────────────────────
|
|
97
|
+
try {
|
|
98
|
+
echo $ok("✓ Listening on {$url}") . PHP_EOL;
|
|
99
|
+
if ($verbose) {
|
|
100
|
+
// Basic error/exception logging
|
|
101
|
+
set_error_handler(function ($severity, $message, $file, $line) use ($err) {
|
|
102
|
+
// Respect @-silence
|
|
103
|
+
if (!(error_reporting() & $severity)) return;
|
|
104
|
+
fwrite(STDERR, $err("PHP Error [{$severity}] {$message} @ {$file}:{$line}\n"));
|
|
105
|
+
});
|
|
106
|
+
set_exception_handler(function (Throwable $e) use ($err) {
|
|
107
|
+
fwrite(STDERR, $err('Uncaught Exception: ' . $e->getMessage() . "\n" . $e->getTraceAsString() . "\n"));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
$server->run(); // blocks until loop->stop()
|
|
112
|
+
|
|
113
|
+
echo PHP_EOL . $ok('✔ Server stopped.') . PHP_EOL;
|
|
114
|
+
exit(0);
|
|
115
|
+
} catch (Throwable $e) {
|
|
116
|
+
fwrite(STDERR, $err('✖ Server error: ' . $e->getMessage()) . PHP_EOL);
|
|
117
|
+
exit(1);
|
|
118
|
+
}
|
package/package.json
CHANGED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
@echo off
|
|
2
|
-
set PORT=8080
|
|
3
|
-
set "PHP_PATH=php"
|
|
4
|
-
set "SERVER_SCRIPT_PATH= src\Lib\Websocket\websocket-server.php"
|
|
5
|
-
|
|
6
|
-
echo [INFO] Checking for processes using port %PORT%...
|
|
7
|
-
netstat -aon | findstr :%PORT%
|
|
8
|
-
|
|
9
|
-
for /f "tokens=5" %%a in ('netstat -aon ^| findstr :%PORT%') do (
|
|
10
|
-
echo [INFO] Found PID: %%a
|
|
11
|
-
taskkill /F /PID %%a
|
|
12
|
-
if %ERRORLEVEL% == 0 (
|
|
13
|
-
echo [SUCCESS] Killed process %%a.
|
|
14
|
-
) else (
|
|
15
|
-
echo [ERROR] Failed to kill process %%a.
|
|
16
|
-
)
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
:: Wait to ensure the port is freed
|
|
20
|
-
timeout /t 2 >nul
|
|
21
|
-
|
|
22
|
-
echo [INFO] Starting WebSocket server on port %PORT%...
|
|
23
|
-
%PHP_PATH% %SERVER_SCRIPT_PATH%
|
|
24
|
-
if %ERRORLEVEL% == 0 (
|
|
25
|
-
echo [SUCCESS] WebSocket server started.
|
|
26
|
-
) else (
|
|
27
|
-
echo [ERROR] Failed to start WebSocket server.
|
|
28
|
-
)
|