create-prisma-php-app 4.0.0-alpha.50 → 4.0.0-alpha.52

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.
@@ -1,663 +0,0 @@
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
- $parts = preg_split('/(<!\[CDATA\[[\s\S]*?\]\]>)/', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
136
- if ($parts === false) {
137
- return $content;
138
- }
139
-
140
- foreach ($parts as $i => $part) {
141
- if (str_starts_with($part, '<![CDATA[')) {
142
- continue;
143
- }
144
-
145
- $parts[$i] = preg_replace(
146
- '/&(?![a-zA-Z][A-Za-z0-9]*;|#[0-9]+;|#x[0-9A-Fa-f]+;)/',
147
- '&amp;',
148
- $part
149
- );
150
- }
151
- return implode('', $parts);
152
- }
153
-
154
- private static function escapeAttributeAngles(string $html): string
155
- {
156
- return preg_replace_callback(
157
- '/(\s[\w:-]+=)([\'"])(.*?)\2/s',
158
- fn($m) => $m[1] . $m[2] . str_replace(['<', '>'], ['&lt;', '&gt;'], $m[3]) . $m[2],
159
- $html
160
- );
161
- }
162
-
163
- private static function escapeMustacheAngles(string $content): string
164
- {
165
- return preg_replace_callback(
166
- '/\{\{[\s\S]*?\}\}/u',
167
- fn($m) => str_replace(['<', '>'], ['&lt;', '&gt;'], $m[0]),
168
- $content
169
- );
170
- }
171
-
172
- public static function convertToXml(string $templateContent): DOMDocument
173
- {
174
- $content = self::protectInlineScripts($templateContent);
175
- $content = self::normalizeNamedEntities($content);
176
-
177
- $content = self::escapeAmpersands($content);
178
- $content = self::escapeAttributeAngles($content);
179
- $content = self::escapeMustacheAngles($content);
180
-
181
- $xml = "<root>{$content}</root>";
182
-
183
- $dom = new DOMDocument('1.0', 'UTF-8');
184
- libxml_use_internal_errors(true);
185
- if (!$dom->loadXML($xml, LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_NONET)) {
186
- throw new RuntimeException(
187
- 'XML Parsing Failed: ' . implode('; ', self::getXmlErrors())
188
- );
189
- }
190
- libxml_clear_errors();
191
- libxml_use_internal_errors(false);
192
- return $dom;
193
- }
194
-
195
- private static function normalizeNamedEntities(string $html): string
196
- {
197
- $parts = preg_split('/(<!\[CDATA\[[\s\S]*?\]\]>)/', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
198
- if ($parts === false) {
199
- return $html;
200
- }
201
-
202
- foreach ($parts as $i => $part) {
203
- if (str_starts_with($part, '<![CDATA[')) {
204
- continue;
205
- }
206
-
207
- $parts[$i] = preg_replace_callback(
208
- '/&([a-zA-Z][a-zA-Z0-9]+);/',
209
- static function (array $m): string {
210
- $decoded = html_entity_decode($m[0], ENT_HTML5, 'UTF-8');
211
- if ($decoded === $m[0]) {
212
- return $m[0];
213
- }
214
- if (function_exists('mb_ord')) {
215
- return '&#' . mb_ord($decoded, 'UTF-8') . ';';
216
- }
217
- $code = unpack('N', mb_convert_encoding($decoded, 'UCS-4BE', 'UTF-8'))[1];
218
- return '&#' . $code . ';';
219
- },
220
- $part
221
- );
222
- }
223
-
224
- return implode('', $parts);
225
- }
226
-
227
- private static function protectInlineScripts(string $html): string
228
- {
229
- if (stripos($html, '<script') === false) {
230
- return $html;
231
- }
232
-
233
- $processScripts = static function (string $content): string {
234
- $callback = static function (array $m): string {
235
- if (preg_match('/\bsrc\s*=/i', $m[1])) {
236
- return $m[0];
237
- }
238
-
239
- if (str_contains($m[2], '<![CDATA[')) {
240
- return $m[0];
241
- }
242
-
243
- $type = '';
244
- if (preg_match('/\btype\s*=\s*([\'"]?)([^\'"\s>]+)/i', $m[1], $t)) {
245
- $type = strtolower($t[2]);
246
- }
247
-
248
- $codeTypes = [
249
- '',
250
- 'text/javascript',
251
- 'application/javascript',
252
- 'module',
253
- 'text/php',
254
- ];
255
-
256
- if (!in_array($type, $codeTypes, true)) {
257
- return $m[0];
258
- }
259
-
260
- $code = str_replace(']]>', ']]]]><![CDATA[>', $m[2]);
261
-
262
- return "<script{$m[1]}><![CDATA[\n{$code}\n]]></script>";
263
- };
264
-
265
- $result = preg_replace_callback(
266
- '#<script\b([^>]*?)>(.*?)</script>#is',
267
- $callback,
268
- $content
269
- );
270
-
271
- if ($result === null) {
272
- $result = preg_replace_callback(
273
- '#<script\b([^>]*?)>(.*?)</script>#is',
274
- $callback,
275
- $content
276
- );
277
-
278
- return $result ?? $content;
279
- }
280
-
281
- return $result;
282
- };
283
-
284
- if (preg_match('/^(.*?<body\b[^>]*>)(.*?)(<\/body>.*)$/is', $html, $parts)) {
285
- [$all, $beforeBody, $body, $afterBody] = $parts;
286
- return $beforeBody . $processScripts($body) . $afterBody;
287
- }
288
-
289
- return $processScripts($html);
290
- }
291
-
292
- public static function innerXml(DOMNode $node): string
293
- {
294
- if ($node instanceof DOMDocument) {
295
- $node = $node->documentElement;
296
- }
297
-
298
- /** @var DOMDocument $doc */
299
- $doc = $node->ownerDocument;
300
-
301
- $html = '';
302
- foreach ($node->childNodes as $child) {
303
- $html .= $doc->saveXML($child);
304
- }
305
- return $html;
306
- }
307
-
308
- protected static function getXmlErrors(): array
309
- {
310
- $errors = libxml_get_errors();
311
- libxml_clear_errors();
312
- return array_map(fn($e) => self::formatLibxmlError($e), $errors);
313
- }
314
-
315
- protected static function formatLibxmlError(LibXMLError $error): string
316
- {
317
- $type = match ($error->level) {
318
- LIBXML_ERR_WARNING => 'Warning',
319
- LIBXML_ERR_ERROR => 'Error',
320
- LIBXML_ERR_FATAL => 'Fatal',
321
- default => 'Unknown',
322
- };
323
- return sprintf(
324
- "[%s] Line %d, Col %d: %s",
325
- $type,
326
- $error->line,
327
- $error->column,
328
- trim($error->message)
329
- );
330
- }
331
-
332
- protected static function processNode(DOMNode $node): string
333
- {
334
- if ($node instanceof DOMText) {
335
- return self::processTextNode($node);
336
- }
337
-
338
- if ($node instanceof DOMElement) {
339
- $pushed = false;
340
- $tag = strtolower($node->nodeName);
341
-
342
- if (
343
- $tag === 'script' &&
344
- !$node->hasAttribute('src') &&
345
- !$node->hasAttribute('type')
346
- ) {
347
- $node->setAttribute('type', 'text/php');
348
- }
349
-
350
- if ($node->hasAttribute('pp-component')) {
351
- self::$sectionStack[] = $node->getAttribute('pp-component');
352
- $pushed = true;
353
- }
354
-
355
- self::processAttributes($node);
356
-
357
- if (isset(self::$classMappings[$node->nodeName])) {
358
- $html = self::renderComponent(
359
- $node,
360
- $node->nodeName,
361
- self::getNodeAttributes($node)
362
- );
363
- if ($pushed) {
364
- array_pop(self::$sectionStack);
365
- }
366
- return $html;
367
- }
368
-
369
- $children = '';
370
- foreach ($node->childNodes as $c) {
371
- $children .= self::processNode($c);
372
- }
373
- $attrs = self::getNodeAttributes($node) + ['children' => $children];
374
- $out = self::renderAsHtml($node->nodeName, $attrs);
375
-
376
- if ($pushed) {
377
- array_pop(self::$sectionStack);
378
- }
379
- return $out;
380
- }
381
-
382
- if ($node instanceof DOMComment) {
383
- return "<!--{$node->textContent}-->";
384
- }
385
-
386
- return $node->textContent;
387
- }
388
-
389
- private static function processTextNode(DOMText $node): string
390
- {
391
- $parent = strtolower($node->parentNode?->nodeName ?? '');
392
- if (isset(self::LITERAL_TEXT_TAGS[$parent])) {
393
- return htmlspecialchars(
394
- $node->textContent,
395
- ENT_NOQUOTES | ENT_SUBSTITUTE,
396
- 'UTF-8'
397
- );
398
- }
399
-
400
- return preg_replace_callback(
401
- self::BINDING_REGEX,
402
- fn($m) => self::processBindingExpression(trim($m[1])),
403
- $node->textContent
404
- );
405
- }
406
-
407
- private static function processAttributes(DOMElement $node): void
408
- {
409
- foreach ($node->attributes as $a) {
410
- if (!preg_match(self::BINDING_REGEX, $a->value, $m)) {
411
- continue;
412
- }
413
-
414
- $rawExpr = trim($m[1]);
415
- $node->setAttribute("pp-bind-{$a->name}", $rawExpr);
416
- }
417
- }
418
-
419
- private static function processBindingExpression(string $expr): string
420
- {
421
- $escaped = htmlspecialchars($expr, ENT_QUOTES, 'UTF-8');
422
-
423
- if (preg_match('/^[\w.]+$/u', $expr)) {
424
- return "<span pp-bind=\"{$escaped}\"></span>";
425
- }
426
-
427
- return "<span pp-bind-expr=\"{$escaped}\"></span>";
428
- }
429
-
430
- protected static function renderComponent(
431
- DOMElement $node,
432
- string $componentName,
433
- array $incomingProps
434
- ): string {
435
- $mapping = self::selectComponentMapping($componentName);
436
-
437
- $baseId = 's' . base_convert(sprintf('%u', crc32($mapping['className'])), 10, 36);
438
- $idx = self::$componentInstanceCounts[$baseId] ?? 0;
439
- self::$componentInstanceCounts[$baseId] = $idx + 1;
440
- $sectionId = $idx === 0 ? $baseId : "{$baseId}{$idx}";
441
-
442
- $originalStack = self::$sectionStack;
443
- self::$sectionStack[] = $sectionId;
444
-
445
- PHPX::setRenderingContext($originalStack, $sectionId);
446
-
447
- $instance = self::initializeComponentInstance($mapping, $incomingProps);
448
-
449
- $childHtml = '';
450
- foreach ($node->childNodes as $c) {
451
- $childHtml .= self::processNode($c);
452
- }
453
-
454
- self::$sectionStack = $originalStack;
455
-
456
- $instance->children = trim($childHtml);
457
-
458
- PHPX::setRenderingContext($originalStack, $sectionId);
459
-
460
- $html = $instance->render();
461
- $html = self::preprocessFragmentSyntax($html);
462
-
463
- $fragDom = self::convertToXml($html);
464
- $root = $fragDom->documentElement;
465
- foreach ($root->childNodes as $c) {
466
- if ($c instanceof DOMElement) {
467
- $c->setAttribute('pp-component', $sectionId);
468
- break;
469
- }
470
- }
471
-
472
- $htmlOut = self::innerXml($fragDom);
473
- $htmlOut = preg_replace_callback(
474
- '/<([a-z0-9-]+)([^>]*)\/>/i',
475
- fn($m) => in_array(strtolower($m[1]), self::$selfClosingTags, true)
476
- ? $m[0]
477
- : "<{$m[1]}{$m[2]}></{$m[1]}>",
478
- $htmlOut
479
- );
480
-
481
- if (
482
- str_contains($htmlOut, '{{') ||
483
- self::hasComponentTag($htmlOut) ||
484
- stripos($htmlOut, '<script') !== false
485
- ) {
486
- $htmlOut = self::compile($htmlOut);
487
- }
488
-
489
- return $htmlOut;
490
- }
491
-
492
- private static function preprocessFragmentSyntax(string $content): string
493
- {
494
- $content = preg_replace('/<>/', '<Fragment>', $content);
495
- $content = preg_replace('/<\/>/', '</Fragment>', $content);
496
-
497
- return $content;
498
- }
499
-
500
- private static function selectComponentMapping(string $componentName): array
501
- {
502
- if (!isset(self::$classMappings[$componentName])) {
503
- throw new RuntimeException("Component {$componentName} not registered");
504
- }
505
- $mappings = self::$classMappings[$componentName];
506
-
507
- $srcNorm = str_replace('\\', '/', SRC_PATH) . '/';
508
- $relImp = str_replace($srcNorm, '', str_replace('\\', '/', Bootstrap::$contentToInclude));
509
-
510
- if (isset($mappings[0]) && is_array($mappings[0])) {
511
- foreach ($mappings as $entry) {
512
- $imp = isset($entry['importer'])
513
- ? str_replace('\\', '/', $entry['importer'])
514
- : '';
515
- if (str_replace($srcNorm, '', $imp) === $relImp) {
516
- return $entry;
517
- }
518
- }
519
- return $mappings[0];
520
- }
521
- return $mappings;
522
- }
523
-
524
- protected static function initializeComponentInstance(array $mapping, array $attributes)
525
- {
526
- if (!isset($mapping['className'], $mapping['filePath'])) {
527
- throw new RuntimeException("Invalid mapping");
528
- }
529
-
530
- $className = $mapping['className'];
531
- $filePath = $mapping['filePath'];
532
-
533
- require_once str_replace('\\', '/', SRC_PATH . '/' . $filePath);
534
- if (!class_exists($className)) {
535
- throw new RuntimeException("Class {$className} not found");
536
- }
537
-
538
- self::cacheClassReflection($className);
539
-
540
- if (!isset(self::$reflections[$className])) {
541
- $rc = new ReflectionClass($className);
542
- self::$reflections[$className] = $rc;
543
- self::$constructors[$className] = $rc->getConstructor();
544
- self::$publicProperties[$className] = array_filter(
545
- $rc->getProperties(ReflectionProperty::IS_PUBLIC),
546
- fn(ReflectionProperty $p) => !$p->isStatic()
547
- );
548
- }
549
-
550
- self::validateComponentProps($className, $attributes);
551
-
552
- $ref = self::$reflections[$className];
553
- $ctor = self::$constructors[$className];
554
- $inst = $ref->newInstanceWithoutConstructor();
555
-
556
- foreach (self::$publicProperties[$className] as $prop) {
557
- $name = $prop->getName();
558
-
559
- if (!array_key_exists($name, $attributes)) {
560
- continue;
561
- }
562
- $value = self::coerce($attributes[$name], $prop->getType());
563
- $prop->setValue($inst, $value);
564
- }
565
-
566
- if ($ctor) {
567
- $ctor->invoke($inst, $attributes);
568
- }
569
-
570
- return $inst;
571
- }
572
-
573
- private static function cacheClassReflection(string $className): void
574
- {
575
- if (isset(self::$reflections[$className])) {
576
- return;
577
- }
578
-
579
- $rc = new ReflectionClass($className);
580
- self::$reflections[$className] = $rc;
581
- self::$constructors[$className] = $rc->getConstructor();
582
-
583
- $publicProps = array_filter(
584
- $rc->getProperties(ReflectionProperty::IS_PUBLIC),
585
- fn(ReflectionProperty $p) => !$p->isStatic()
586
- );
587
- self::$publicProperties[$className] = $publicProps;
588
-
589
- $allowed = self::SYSTEM_PROPS;
590
- foreach ($publicProps as $prop) {
591
- $allowed[$prop->getName()] = true;
592
- }
593
- self::$allowedProps[$className] = $allowed;
594
- }
595
-
596
- private static function validateComponentProps(string $className, array $attributes): void
597
- {
598
- foreach (self::$publicProperties[$className] as $prop) {
599
- $name = $prop->getName();
600
- $type = $prop->getType();
601
-
602
- if (
603
- $type instanceof ReflectionNamedType && $type->isBuiltin()
604
- && ! $type->allowsNull()
605
- && ! array_key_exists($name, $attributes)
606
- ) {
607
- throw new ComponentValidationException(
608
- $name,
609
- $className,
610
- array_map(fn($p) => $p->getName(), self::$publicProperties[$className])
611
- );
612
- }
613
- }
614
-
615
- return;
616
- }
617
-
618
- private static function coerce(mixed $value, ?ReflectionType $type): mixed
619
- {
620
- return TypeCoercer::coerce($value, $type);
621
- }
622
-
623
- protected static function initializeClassMappings(): void
624
- {
625
- foreach (PrismaPHPSettings::$classLogFiles as $tag => $cls) {
626
- self::$classMappings[$tag] = $cls;
627
- }
628
- }
629
-
630
- protected static function hasComponentTag(string $html): bool
631
- {
632
- return preg_match('/<\/*[A-Z][\w-]*/u', $html) === 1;
633
- }
634
-
635
- private static function getNodeAttributes(DOMElement $node): array
636
- {
637
- $out = [];
638
- foreach ($node->attributes as $a) {
639
- $out[$a->name] = $a->value;
640
- }
641
- return $out;
642
- }
643
-
644
- private static function renderAsHtml(string $tag, array $attrs): string
645
- {
646
- $pairs = [];
647
- foreach ($attrs as $k => $v) {
648
- if ($k === 'children') {
649
- continue;
650
- }
651
- $pairs[] = sprintf(
652
- '%s="%s"',
653
- $k,
654
- htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
655
- );
656
- }
657
- $attrStr = $pairs ? ' ' . implode(' ', $pairs) : '';
658
-
659
- return in_array(strtolower($tag), self::$selfClosingTags, true)
660
- ? "<{$tag}{$attrStr} />"
661
- : "<{$tag}{$attrStr}>{$attrs['children']}</{$tag}>";
662
- }
663
- }