create-prisma-php-app 4.0.0-alpha.2 → 4.0.0-alpha.21
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 +143 -98
- package/dist/index.js +264 -99
- package/dist/settings/auto-swagger-docs.ts +196 -95
- package/dist/settings/bs-config.ts +56 -58
- package/dist/settings/files-list.json +1 -1
- package/dist/settings/restart-mcp.ts +58 -0
- package/dist/settings/restart-websocket.ts +51 -45
- package/dist/settings/utils.ts +240 -0
- package/dist/src/Lib/AI/ChatGPTClient.php +147 -0
- package/dist/src/Lib/Auth/Auth.php +544 -0
- package/dist/src/Lib/Auth/AuthConfig.php +89 -0
- package/dist/src/Lib/CacheHandler.php +121 -0
- package/dist/src/Lib/ErrorHandler.php +322 -0
- package/dist/src/Lib/FileManager/UploadFile.php +383 -0
- package/dist/src/Lib/Headers/Boom.php +192 -0
- package/dist/src/Lib/IncludeTracker.php +59 -0
- package/dist/src/Lib/MCP/WeatherTools.php +104 -0
- package/dist/src/Lib/MCP/mcp-server.php +80 -0
- package/dist/src/Lib/MainLayout.php +230 -0
- package/dist/src/Lib/Middleware/AuthMiddleware.php +154 -0
- package/dist/src/Lib/Middleware/CorsMiddleware.php +145 -0
- package/dist/src/Lib/PHPMailer/Mailer.php +169 -0
- package/dist/src/Lib/PHPX/Exceptions/ComponentValidationException.php +49 -0
- package/dist/src/Lib/PHPX/Fragment.php +32 -0
- package/dist/src/Lib/PHPX/IPHPX.php +22 -0
- package/dist/src/Lib/PHPX/PHPX.php +287 -0
- package/dist/src/Lib/PHPX/TemplateCompiler.php +641 -0
- package/dist/src/Lib/PHPX/TwMerge.php +346 -0
- package/dist/src/Lib/PHPX/TypeCoercer.php +490 -0
- package/dist/src/Lib/PartialRenderer.php +40 -0
- package/dist/src/Lib/PrismaPHPSettings.php +181 -0
- package/dist/src/Lib/Request.php +479 -0
- package/dist/src/Lib/Security/RateLimiter.php +33 -0
- package/dist/src/Lib/Set.php +102 -0
- package/dist/src/Lib/StateManager.php +127 -0
- package/dist/src/Lib/Validator.php +752 -0
- package/dist/src/{Websocket → Lib/Websocket}/ConnectionManager.php +1 -1
- package/dist/src/Lib/Websocket/websocket-server.php +118 -0
- package/dist/src/app/error.php +1 -1
- package/dist/src/app/index.php +24 -5
- package/dist/src/app/js/index.js +1 -1
- package/dist/src/app/layout.php +2 -2
- package/package.json +1 -1
- package/dist/settings/restart-websocket.bat +0 -28
- package/dist/src/app/assets/images/prisma-php-black.svg +0 -6
- package/dist/websocket-server.php +0 -22
- package/vendor/autoload.php +0 -25
- package/vendor/composer/ClassLoader.php +0 -579
- package/vendor/composer/InstalledVersions.php +0 -359
- package/vendor/composer/LICENSE +0 -21
- package/vendor/composer/autoload_classmap.php +0 -10
- package/vendor/composer/autoload_namespaces.php +0 -9
- package/vendor/composer/autoload_psr4.php +0 -10
- package/vendor/composer/autoload_real.php +0 -38
- package/vendor/composer/autoload_static.php +0 -25
- package/vendor/composer/installed.json +0 -825
- package/vendor/composer/installed.php +0 -132
- package/vendor/composer/platform_check.php +0 -26
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Lib\PHPX;
|
|
6
|
+
|
|
7
|
+
use Lib\PrismaPHPSettings;
|
|
8
|
+
use Lib\MainLayout;
|
|
9
|
+
use DOMDocument;
|
|
10
|
+
use DOMElement;
|
|
11
|
+
use DOMComment;
|
|
12
|
+
use DOMNode;
|
|
13
|
+
use DOMText;
|
|
14
|
+
use RuntimeException;
|
|
15
|
+
use Bootstrap;
|
|
16
|
+
use LibXMLError;
|
|
17
|
+
use ReflectionClass;
|
|
18
|
+
use ReflectionProperty;
|
|
19
|
+
use ReflectionType;
|
|
20
|
+
use ReflectionNamedType;
|
|
21
|
+
use Lib\PHPX\TypeCoercer;
|
|
22
|
+
use Lib\PHPX\Exceptions\ComponentValidationException;
|
|
23
|
+
|
|
24
|
+
class TemplateCompiler
|
|
25
|
+
{
|
|
26
|
+
protected const BINDING_REGEX = '/\{\{\s*((?:(?!\{\{|\}\})[\s\S])*?)\s*\}\}/uS';
|
|
27
|
+
private const LITERAL_TEXT_TAGS = [
|
|
28
|
+
'code' => true,
|
|
29
|
+
'pre' => true,
|
|
30
|
+
'samp' => true,
|
|
31
|
+
'kbd' => true,
|
|
32
|
+
'var' => true,
|
|
33
|
+
];
|
|
34
|
+
private const SYSTEM_PROPS = [
|
|
35
|
+
'children' => true,
|
|
36
|
+
'key' => true,
|
|
37
|
+
'ref' => true,
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
protected static array $classMappings = [];
|
|
41
|
+
protected static array $selfClosingTags = [
|
|
42
|
+
'area',
|
|
43
|
+
'base',
|
|
44
|
+
'br',
|
|
45
|
+
'col',
|
|
46
|
+
'command',
|
|
47
|
+
'embed',
|
|
48
|
+
'hr',
|
|
49
|
+
'img',
|
|
50
|
+
'input',
|
|
51
|
+
'keygen',
|
|
52
|
+
'link',
|
|
53
|
+
'meta',
|
|
54
|
+
'param',
|
|
55
|
+
'source',
|
|
56
|
+
'track',
|
|
57
|
+
'wbr'
|
|
58
|
+
];
|
|
59
|
+
private static array $sectionStack = [];
|
|
60
|
+
private static int $compileDepth = 0;
|
|
61
|
+
private static array $componentInstanceCounts = [];
|
|
62
|
+
private static array $reflections = [];
|
|
63
|
+
private static array $constructors = [];
|
|
64
|
+
private static array $publicProperties = [];
|
|
65
|
+
private static array $allowedProps = [];
|
|
66
|
+
|
|
67
|
+
public static function compile(string $templateContent): string
|
|
68
|
+
{
|
|
69
|
+
if (self::$compileDepth === 0) {
|
|
70
|
+
self::$componentInstanceCounts = [];
|
|
71
|
+
}
|
|
72
|
+
self::$compileDepth++;
|
|
73
|
+
|
|
74
|
+
if (empty(self::$classMappings)) {
|
|
75
|
+
self::initializeClassMappings();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
$dom = self::convertToXml($templateContent);
|
|
79
|
+
$root = $dom->documentElement;
|
|
80
|
+
|
|
81
|
+
$output = [];
|
|
82
|
+
foreach ($root->childNodes as $child) {
|
|
83
|
+
$output[] = self::processNode($child);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
self::$compileDepth--;
|
|
87
|
+
return implode('', $output);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public static function injectDynamicContent(string $htmlContent): string
|
|
91
|
+
{
|
|
92
|
+
$headOpenPattern = '/(<head\b[^>]*>)/i';
|
|
93
|
+
|
|
94
|
+
$htmlContent = preg_replace(
|
|
95
|
+
$headOpenPattern,
|
|
96
|
+
'$1' . MainLayout::outputMetadata(),
|
|
97
|
+
$htmlContent,
|
|
98
|
+
1
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
$headClosePattern = '/(<\/head\s*>)/i';
|
|
102
|
+
$headScripts = MainLayout::outputHeadScripts();
|
|
103
|
+
$htmlContent = preg_replace(
|
|
104
|
+
$headClosePattern,
|
|
105
|
+
$headScripts . '$1',
|
|
106
|
+
$htmlContent,
|
|
107
|
+
1
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (!isset($_SERVER['HTTP_X_PPHP_NAVIGATION'])) {
|
|
111
|
+
if (!PrismaPHPSettings::$option->backendOnly) {
|
|
112
|
+
$htmlContent = preg_replace(
|
|
113
|
+
'/<body([^>]*)>/i',
|
|
114
|
+
'<body$1 hidden>',
|
|
115
|
+
$htmlContent,
|
|
116
|
+
1
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
$bodyClosePattern = '/(<\/body\s*>)/i';
|
|
122
|
+
|
|
123
|
+
$htmlContent = preg_replace(
|
|
124
|
+
$bodyClosePattern,
|
|
125
|
+
MainLayout::outputFooterScripts() . '$1',
|
|
126
|
+
$htmlContent,
|
|
127
|
+
1
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return $htmlContent;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private static function escapeAmpersands(string $content): string
|
|
134
|
+
{
|
|
135
|
+
return preg_replace(
|
|
136
|
+
'/&(?![a-zA-Z][A-Za-z0-9]*;|#[0-9]+;|#x[0-9A-Fa-f]+;)/',
|
|
137
|
+
'&',
|
|
138
|
+
$content
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private static function escapeAttributeAngles(string $html): string
|
|
143
|
+
{
|
|
144
|
+
return preg_replace_callback(
|
|
145
|
+
'/(\s[\w:-]+=)([\'"])(.*?)\2/s',
|
|
146
|
+
fn($m) => $m[1] . $m[2] . str_replace(['<', '>'], ['<', '>'], $m[3]) . $m[2],
|
|
147
|
+
$html
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private static function escapeMustacheAngles(string $content): string
|
|
152
|
+
{
|
|
153
|
+
return preg_replace_callback(
|
|
154
|
+
'/\{\{[\s\S]*?\}\}/u',
|
|
155
|
+
fn($m) => str_replace(['<', '>'], ['<', '>'], $m[0]),
|
|
156
|
+
$content
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public static function convertToXml(string $templateContent): DOMDocument
|
|
161
|
+
{
|
|
162
|
+
$content = self::protectInlineScripts($templateContent);
|
|
163
|
+
$content = self::normalizeNamedEntities($content);
|
|
164
|
+
|
|
165
|
+
$content = self::escapeAmpersands($content);
|
|
166
|
+
$content = self::escapeAttributeAngles($content);
|
|
167
|
+
$content = self::escapeMustacheAngles($content);
|
|
168
|
+
|
|
169
|
+
$xml = "<root>{$content}</root>";
|
|
170
|
+
|
|
171
|
+
$dom = new DOMDocument('1.0', 'UTF-8');
|
|
172
|
+
libxml_use_internal_errors(true);
|
|
173
|
+
if (!$dom->loadXML($xml, LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_NONET)) {
|
|
174
|
+
throw new RuntimeException(
|
|
175
|
+
'XML Parsing Failed: ' . implode('; ', self::getXmlErrors())
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
libxml_clear_errors();
|
|
179
|
+
libxml_use_internal_errors(false);
|
|
180
|
+
return $dom;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private static function normalizeNamedEntities(string $html): string
|
|
184
|
+
{
|
|
185
|
+
return preg_replace_callback(
|
|
186
|
+
'/&([a-zA-Z][a-zA-Z0-9]+);/',
|
|
187
|
+
static function (array $m): string {
|
|
188
|
+
$decoded = html_entity_decode($m[0], ENT_HTML5, 'UTF-8');
|
|
189
|
+
|
|
190
|
+
if ($decoded === $m[0]) {
|
|
191
|
+
return $m[0];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (function_exists('mb_ord')) {
|
|
195
|
+
return '&#' . mb_ord($decoded, 'UTF-8') . ';';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
$code = unpack('N', mb_convert_encoding($decoded, 'UCS-4BE', 'UTF-8'))[1];
|
|
199
|
+
return '&#' . $code . ';';
|
|
200
|
+
},
|
|
201
|
+
$html
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private static function protectInlineScripts(string $html): string
|
|
206
|
+
{
|
|
207
|
+
if (stripos($html, '<script') === false) {
|
|
208
|
+
return $html;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
$processScripts = static function (string $content): string {
|
|
212
|
+
$callback = static function (array $m): string {
|
|
213
|
+
if (preg_match('/\bsrc\s*=/i', $m[1])) {
|
|
214
|
+
return $m[0];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (str_contains($m[2], '<![CDATA[')) {
|
|
218
|
+
return $m[0];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
$type = '';
|
|
222
|
+
if (preg_match('/\btype\s*=\s*([\'"]?)([^\'"\s>]+)/i', $m[1], $t)) {
|
|
223
|
+
$type = strtolower($t[2]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
$codeTypes = [
|
|
227
|
+
'',
|
|
228
|
+
'text/javascript',
|
|
229
|
+
'application/javascript',
|
|
230
|
+
'module',
|
|
231
|
+
'text/php',
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
if (!in_array($type, $codeTypes, true)) {
|
|
235
|
+
return $m[0];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
$code = str_replace(']]>', ']]]]><![CDATA[>', $m[2]);
|
|
239
|
+
|
|
240
|
+
return "<script{$m[1]}><![CDATA[\n{$code}\n]]></script>";
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
$result = preg_replace_callback(
|
|
244
|
+
'#<script\b([^>]*?)>(.*?)</script>#is',
|
|
245
|
+
$callback,
|
|
246
|
+
$content
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
if ($result === null) {
|
|
250
|
+
$result = preg_replace_callback(
|
|
251
|
+
'#<script\b([^>]*?)>(.*?)</script>#is',
|
|
252
|
+
$callback,
|
|
253
|
+
$content
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return $result ?? $content;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return $result;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
if (preg_match('/^(.*?<body\b[^>]*>)(.*?)(<\/body>.*)$/is', $html, $parts)) {
|
|
263
|
+
[$all, $beforeBody, $body, $afterBody] = $parts;
|
|
264
|
+
return $beforeBody . $processScripts($body) . $afterBody;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return $processScripts($html);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
public static function innerXml(DOMNode $node): string
|
|
271
|
+
{
|
|
272
|
+
if ($node instanceof DOMDocument) {
|
|
273
|
+
$node = $node->documentElement;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** @var DOMDocument $doc */
|
|
277
|
+
$doc = $node->ownerDocument;
|
|
278
|
+
|
|
279
|
+
$html = '';
|
|
280
|
+
foreach ($node->childNodes as $child) {
|
|
281
|
+
$html .= $doc->saveXML($child);
|
|
282
|
+
}
|
|
283
|
+
return $html;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
protected static function getXmlErrors(): array
|
|
287
|
+
{
|
|
288
|
+
$errors = libxml_get_errors();
|
|
289
|
+
libxml_clear_errors();
|
|
290
|
+
return array_map(fn($e) => self::formatLibxmlError($e), $errors);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
protected static function formatLibxmlError(LibXMLError $error): string
|
|
294
|
+
{
|
|
295
|
+
$type = match ($error->level) {
|
|
296
|
+
LIBXML_ERR_WARNING => 'Warning',
|
|
297
|
+
LIBXML_ERR_ERROR => 'Error',
|
|
298
|
+
LIBXML_ERR_FATAL => 'Fatal',
|
|
299
|
+
default => 'Unknown',
|
|
300
|
+
};
|
|
301
|
+
return sprintf(
|
|
302
|
+
"[%s] Line %d, Col %d: %s",
|
|
303
|
+
$type,
|
|
304
|
+
$error->line,
|
|
305
|
+
$error->column,
|
|
306
|
+
trim($error->message)
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
protected static function processNode(DOMNode $node): string
|
|
311
|
+
{
|
|
312
|
+
if ($node instanceof DOMText) {
|
|
313
|
+
return self::processTextNode($node);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if ($node instanceof DOMElement) {
|
|
317
|
+
$pushed = false;
|
|
318
|
+
$tag = strtolower($node->nodeName);
|
|
319
|
+
|
|
320
|
+
if (
|
|
321
|
+
$tag === 'script' &&
|
|
322
|
+
!$node->hasAttribute('src') &&
|
|
323
|
+
!$node->hasAttribute('type')
|
|
324
|
+
) {
|
|
325
|
+
$node->setAttribute('type', 'text/php');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if ($node->hasAttribute('pp-component')) {
|
|
329
|
+
self::$sectionStack[] = $node->getAttribute('pp-component');
|
|
330
|
+
$pushed = true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
self::processAttributes($node);
|
|
334
|
+
|
|
335
|
+
if (isset(self::$classMappings[$node->nodeName])) {
|
|
336
|
+
$html = self::renderComponent(
|
|
337
|
+
$node,
|
|
338
|
+
$node->nodeName,
|
|
339
|
+
self::getNodeAttributes($node)
|
|
340
|
+
);
|
|
341
|
+
if ($pushed) {
|
|
342
|
+
array_pop(self::$sectionStack);
|
|
343
|
+
}
|
|
344
|
+
return $html;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
$children = '';
|
|
348
|
+
foreach ($node->childNodes as $c) {
|
|
349
|
+
$children .= self::processNode($c);
|
|
350
|
+
}
|
|
351
|
+
$attrs = self::getNodeAttributes($node) + ['children' => $children];
|
|
352
|
+
$out = self::renderAsHtml($node->nodeName, $attrs);
|
|
353
|
+
|
|
354
|
+
if ($pushed) {
|
|
355
|
+
array_pop(self::$sectionStack);
|
|
356
|
+
}
|
|
357
|
+
return $out;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if ($node instanceof DOMComment) {
|
|
361
|
+
return "<!--{$node->textContent}-->";
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return $node->textContent;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private static function processTextNode(DOMText $node): string
|
|
368
|
+
{
|
|
369
|
+
$parent = strtolower($node->parentNode?->nodeName ?? '');
|
|
370
|
+
if (isset(self::LITERAL_TEXT_TAGS[$parent])) {
|
|
371
|
+
return htmlspecialchars(
|
|
372
|
+
$node->textContent,
|
|
373
|
+
ENT_NOQUOTES | ENT_SUBSTITUTE,
|
|
374
|
+
'UTF-8'
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return preg_replace_callback(
|
|
379
|
+
self::BINDING_REGEX,
|
|
380
|
+
fn($m) => self::processBindingExpression(trim($m[1])),
|
|
381
|
+
$node->textContent
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private static function processAttributes(DOMElement $node): void
|
|
386
|
+
{
|
|
387
|
+
foreach ($node->attributes as $a) {
|
|
388
|
+
if (!preg_match(self::BINDING_REGEX, $a->value, $m)) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
$rawExpr = trim($m[1]);
|
|
393
|
+
$node->setAttribute("pp-bind-{$a->name}", $rawExpr);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private static function processBindingExpression(string $expr): string
|
|
398
|
+
{
|
|
399
|
+
$escaped = htmlspecialchars($expr, ENT_QUOTES, 'UTF-8');
|
|
400
|
+
|
|
401
|
+
if (preg_match('/^[\w.]+$/u', $expr)) {
|
|
402
|
+
return "<span pp-bind=\"{$escaped}\"></span>";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return "<span pp-bind-expr=\"{$escaped}\"></span>";
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
protected static function renderComponent(
|
|
409
|
+
DOMElement $node,
|
|
410
|
+
string $componentName,
|
|
411
|
+
array $incomingProps
|
|
412
|
+
): string {
|
|
413
|
+
$mapping = self::selectComponentMapping($componentName);
|
|
414
|
+
|
|
415
|
+
$baseId = 's' . base_convert(sprintf('%u', crc32($mapping['className'])), 10, 36);
|
|
416
|
+
$idx = self::$componentInstanceCounts[$baseId] ?? 0;
|
|
417
|
+
self::$componentInstanceCounts[$baseId] = $idx + 1;
|
|
418
|
+
$sectionId = $idx === 0 ? $baseId : "{$baseId}{$idx}";
|
|
419
|
+
|
|
420
|
+
$originalStack = self::$sectionStack;
|
|
421
|
+
self::$sectionStack[] = $sectionId;
|
|
422
|
+
|
|
423
|
+
PHPX::setRenderingContext($originalStack, $sectionId);
|
|
424
|
+
|
|
425
|
+
$instance = self::initializeComponentInstance($mapping, $incomingProps);
|
|
426
|
+
|
|
427
|
+
$childHtml = '';
|
|
428
|
+
foreach ($node->childNodes as $c) {
|
|
429
|
+
$childHtml .= self::processNode($c);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
self::$sectionStack = $originalStack;
|
|
433
|
+
|
|
434
|
+
$instance->children = trim($childHtml);
|
|
435
|
+
|
|
436
|
+
PHPX::setRenderingContext($originalStack, $sectionId);
|
|
437
|
+
|
|
438
|
+
$html = $instance->render();
|
|
439
|
+
$html = self::preprocessFragmentSyntax($html);
|
|
440
|
+
|
|
441
|
+
$fragDom = self::convertToXml($html);
|
|
442
|
+
$root = $fragDom->documentElement;
|
|
443
|
+
foreach ($root->childNodes as $c) {
|
|
444
|
+
if ($c instanceof DOMElement) {
|
|
445
|
+
$c->setAttribute('pp-component', $sectionId);
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
$htmlOut = self::innerXml($fragDom);
|
|
451
|
+
$htmlOut = preg_replace_callback(
|
|
452
|
+
'/<([a-z0-9-]+)([^>]*)\/>/i',
|
|
453
|
+
fn($m) => in_array(strtolower($m[1]), self::$selfClosingTags, true)
|
|
454
|
+
? $m[0]
|
|
455
|
+
: "<{$m[1]}{$m[2]}></{$m[1]}>",
|
|
456
|
+
$htmlOut
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
if (
|
|
460
|
+
str_contains($htmlOut, '{{') ||
|
|
461
|
+
self::hasComponentTag($htmlOut) ||
|
|
462
|
+
stripos($htmlOut, '<script') !== false
|
|
463
|
+
) {
|
|
464
|
+
$htmlOut = self::compile($htmlOut);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return $htmlOut;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private static function preprocessFragmentSyntax(string $content): string
|
|
471
|
+
{
|
|
472
|
+
$content = preg_replace('/<>/', '<Fragment>', $content);
|
|
473
|
+
$content = preg_replace('/<\/>/', '</Fragment>', $content);
|
|
474
|
+
|
|
475
|
+
return $content;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private static function selectComponentMapping(string $componentName): array
|
|
479
|
+
{
|
|
480
|
+
if (!isset(self::$classMappings[$componentName])) {
|
|
481
|
+
throw new RuntimeException("Component {$componentName} not registered");
|
|
482
|
+
}
|
|
483
|
+
$mappings = self::$classMappings[$componentName];
|
|
484
|
+
|
|
485
|
+
$srcNorm = str_replace('\\', '/', SRC_PATH) . '/';
|
|
486
|
+
$relImp = str_replace($srcNorm, '', str_replace('\\', '/', Bootstrap::$contentToInclude));
|
|
487
|
+
|
|
488
|
+
if (isset($mappings[0]) && is_array($mappings[0])) {
|
|
489
|
+
foreach ($mappings as $entry) {
|
|
490
|
+
$imp = isset($entry['importer'])
|
|
491
|
+
? str_replace('\\', '/', $entry['importer'])
|
|
492
|
+
: '';
|
|
493
|
+
if (str_replace($srcNorm, '', $imp) === $relImp) {
|
|
494
|
+
return $entry;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return $mappings[0];
|
|
498
|
+
}
|
|
499
|
+
return $mappings;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
protected static function initializeComponentInstance(array $mapping, array $attributes)
|
|
503
|
+
{
|
|
504
|
+
if (!isset($mapping['className'], $mapping['filePath'])) {
|
|
505
|
+
throw new RuntimeException("Invalid mapping");
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
$className = $mapping['className'];
|
|
509
|
+
$filePath = $mapping['filePath'];
|
|
510
|
+
|
|
511
|
+
require_once str_replace('\\', '/', SRC_PATH . '/' . $filePath);
|
|
512
|
+
if (!class_exists($className)) {
|
|
513
|
+
throw new RuntimeException("Class {$className} not found");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
self::cacheClassReflection($className);
|
|
517
|
+
|
|
518
|
+
if (!isset(self::$reflections[$className])) {
|
|
519
|
+
$rc = new ReflectionClass($className);
|
|
520
|
+
self::$reflections[$className] = $rc;
|
|
521
|
+
self::$constructors[$className] = $rc->getConstructor();
|
|
522
|
+
self::$publicProperties[$className] = array_filter(
|
|
523
|
+
$rc->getProperties(ReflectionProperty::IS_PUBLIC),
|
|
524
|
+
fn(ReflectionProperty $p) => !$p->isStatic()
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
self::validateComponentProps($className, $attributes);
|
|
529
|
+
|
|
530
|
+
$ref = self::$reflections[$className];
|
|
531
|
+
$ctor = self::$constructors[$className];
|
|
532
|
+
$inst = $ref->newInstanceWithoutConstructor();
|
|
533
|
+
|
|
534
|
+
foreach (self::$publicProperties[$className] as $prop) {
|
|
535
|
+
$name = $prop->getName();
|
|
536
|
+
|
|
537
|
+
if (!array_key_exists($name, $attributes)) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
$value = self::coerce($attributes[$name], $prop->getType());
|
|
541
|
+
$prop->setValue($inst, $value);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if ($ctor) {
|
|
545
|
+
$ctor->invoke($inst, $attributes);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return $inst;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private static function cacheClassReflection(string $className): void
|
|
552
|
+
{
|
|
553
|
+
if (isset(self::$reflections[$className])) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
$rc = new ReflectionClass($className);
|
|
558
|
+
self::$reflections[$className] = $rc;
|
|
559
|
+
self::$constructors[$className] = $rc->getConstructor();
|
|
560
|
+
|
|
561
|
+
$publicProps = array_filter(
|
|
562
|
+
$rc->getProperties(ReflectionProperty::IS_PUBLIC),
|
|
563
|
+
fn(ReflectionProperty $p) => !$p->isStatic()
|
|
564
|
+
);
|
|
565
|
+
self::$publicProperties[$className] = $publicProps;
|
|
566
|
+
|
|
567
|
+
$allowed = self::SYSTEM_PROPS;
|
|
568
|
+
foreach ($publicProps as $prop) {
|
|
569
|
+
$allowed[$prop->getName()] = true;
|
|
570
|
+
}
|
|
571
|
+
self::$allowedProps[$className] = $allowed;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private static function validateComponentProps(string $className, array $attributes): void
|
|
575
|
+
{
|
|
576
|
+
foreach (self::$publicProperties[$className] as $prop) {
|
|
577
|
+
$name = $prop->getName();
|
|
578
|
+
$type = $prop->getType();
|
|
579
|
+
|
|
580
|
+
if (
|
|
581
|
+
$type instanceof ReflectionNamedType && $type->isBuiltin()
|
|
582
|
+
&& ! $type->allowsNull()
|
|
583
|
+
&& ! array_key_exists($name, $attributes)
|
|
584
|
+
) {
|
|
585
|
+
throw new ComponentValidationException(
|
|
586
|
+
$name,
|
|
587
|
+
$className,
|
|
588
|
+
array_map(fn($p) => $p->getName(), self::$publicProperties[$className])
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private static function coerce(mixed $value, ?ReflectionType $type): mixed
|
|
597
|
+
{
|
|
598
|
+
return TypeCoercer::coerce($value, $type);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
protected static function initializeClassMappings(): void
|
|
602
|
+
{
|
|
603
|
+
foreach (PrismaPHPSettings::$classLogFiles as $tag => $cls) {
|
|
604
|
+
self::$classMappings[$tag] = $cls;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
protected static function hasComponentTag(string $html): bool
|
|
609
|
+
{
|
|
610
|
+
return preg_match('/<\/*[A-Z][\w-]*/u', $html) === 1;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private static function getNodeAttributes(DOMElement $node): array
|
|
614
|
+
{
|
|
615
|
+
$out = [];
|
|
616
|
+
foreach ($node->attributes as $a) {
|
|
617
|
+
$out[$a->name] = $a->value;
|
|
618
|
+
}
|
|
619
|
+
return $out;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private static function renderAsHtml(string $tag, array $attrs): string
|
|
623
|
+
{
|
|
624
|
+
$pairs = [];
|
|
625
|
+
foreach ($attrs as $k => $v) {
|
|
626
|
+
if ($k === 'children') {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
$pairs[] = sprintf(
|
|
630
|
+
'%s="%s"',
|
|
631
|
+
$k,
|
|
632
|
+
htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
$attrStr = $pairs ? ' ' . implode(' ', $pairs) : '';
|
|
636
|
+
|
|
637
|
+
return in_array(strtolower($tag), self::$selfClosingTags, true)
|
|
638
|
+
? "<{$tag}{$attrStr} />"
|
|
639
|
+
: "<{$tag}{$attrStr}>{$attrs['children']}</{$tag}>";
|
|
640
|
+
}
|
|
641
|
+
}
|