create-prisma-php-app 3.6.3 → 3.6.5

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