create-prisma-php-app 3.6.2 → 3.6.4

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.
@@ -6,29 +6,22 @@ namespace Lib\Middleware;
6
6
 
7
7
  final class CorsMiddleware
8
8
  {
9
- /** Entry point */
10
9
  public static function handle(?array $overrides = null): void
11
10
  {
12
- // Not a CORS request
13
11
  $origin = $_SERVER['HTTP_ORIGIN'] ?? '';
14
12
  if ($origin === '') {
15
13
  return;
16
14
  }
17
15
 
18
- // Resolve config (env → overrides)
19
16
  $cfg = self::buildConfig($overrides);
20
17
 
21
- // Not allowed? Do nothing (browser will block)
22
18
  if (!self::isAllowedOrigin($origin, $cfg['allowedOrigins'])) {
23
19
  return;
24
20
  }
25
21
 
26
- // Compute which value to send for Access-Control-Allow-Origin
27
- // If credentials are disabled and '*' is in list, we can send '*'
28
22
  $sendWildcard = (!$cfg['allowCredentials'] && self::listHasWildcard($cfg['allowedOrigins']));
29
23
  $allowOriginValue = $sendWildcard ? '*' : self::normalize($origin);
30
24
 
31
- // Vary for caches
32
25
  header('Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers');
33
26
 
34
27
  header('Access-Control-Allow-Origin: ' . $allowOriginValue);
@@ -37,7 +30,6 @@ final class CorsMiddleware
37
30
  }
38
31
 
39
32
  if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
40
- // Preflight response
41
33
  $requestedHeaders = $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'] ?? '';
42
34
  $allowedHeaders = $cfg['allowedHeaders'] !== ''
43
35
  ? $cfg['allowedHeaders']
@@ -49,7 +41,6 @@ final class CorsMiddleware
49
41
  header('Access-Control-Max-Age: ' . (string) $cfg['maxAge']);
50
42
  }
51
43
 
52
- // Optional: Private Network Access preflights (Chrome)
53
44
  if (!empty($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK'])) {
54
45
  header('Access-Control-Allow-Private-Network: true');
55
46
  }
@@ -59,13 +50,11 @@ final class CorsMiddleware
59
50
  exit;
60
51
  }
61
52
 
62
- // Simple/actual request
63
53
  if ($cfg['exposeHeaders'] !== '') {
64
54
  header('Access-Control-Expose-Headers: ' . $cfg['exposeHeaders']);
65
55
  }
66
56
  }
67
57
 
68
- /** Read env + normalize + apply overrides */
69
58
  private static function buildConfig(?array $overrides): array
70
59
  {
71
60
  $allowed = self::parseList($_ENV['CORS_ALLOWED_ORIGINS'] ?? '');
@@ -86,12 +75,10 @@ final class CorsMiddleware
86
75
  }
87
76
  }
88
77
 
89
- // Normalize patterns
90
78
  $cfg['allowedOrigins'] = array_map([self::class, 'normalize'], $cfg['allowedOrigins']);
91
79
  return $cfg;
92
80
  }
93
81
 
94
- /** CSV or JSON array → array<string> */
95
82
  private static function parseList(string $raw): array
96
83
  {
97
84
  $raw = trim($raw);
@@ -118,13 +105,10 @@ final class CorsMiddleware
118
105
  foreach ($list as $pattern) {
119
106
  $p = self::normalize($pattern);
120
107
 
121
- // literal "*"
122
108
  if ($p === '*') return true;
123
109
 
124
- // allow literal "null" for file:// or sandboxed if explicitly listed
125
110
  if ($o === 'null' && strtolower($p) === 'null') return true;
126
111
 
127
- // wildcard like https://*.example.com
128
112
  if (strpos($p, '*') !== false) {
129
113
  $regex = '/^' . str_replace('\*', '[^.]+', preg_quote($p, '/')) . '$/i';
130
114
  if (preg_match($regex, $o)) return true;
@@ -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,302 +92,292 @@ 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
+ ];
116
+
117
+ foreach ($replacements as $pattern => $replacement) {
118
+ $htmlContent = preg_replace($pattern, $replacement, $htmlContent, 1);
119
+ }
80
120
 
81
- $output = [];
82
- foreach ($root->childNodes as $child) {
83
- $output[] = self::processNode($child);
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
+ );
84
131
  }
85
132
 
86
- self::$compileDepth--;
87
- return implode('', $output);
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
- }
159
+ private static function createDomFromXml(string $xml): DOMDocument
160
+ {
161
+ $dom = new DOMDocument('1.0', 'UTF-8');
162
+ libxml_use_internal_errors(true);
163
+
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));
119
168
  }
120
169
 
121
- $bodyClosePattern = '/(<\/body\s*>)/i';
170
+ libxml_clear_errors();
171
+ libxml_use_internal_errors(false);
122
172
 
123
- $htmlContent = preg_replace(
124
- $bodyClosePattern,
125
- MainLayout::outputFooterScripts() . '$1',
126
- $htmlContent,
127
- 1
128
- );
173
+ return $dom;
174
+ }
129
175
 
130
- return $htmlContent;
176
+ private static function processChildNodes($childNodes): array
177
+ {
178
+ $output = [];
179
+ foreach ($childNodes as $child) {
180
+ $output[] = self::processNode($child);
181
+ }
182
+ return $output;
131
183
  }
132
184
 
133
185
  private static function escapeAmpersands(string $content): string
134
186
  {
135
- return preg_replace(
136
- '/&(?![a-zA-Z][A-Za-z0-9]*;|#[0-9]+;|#x[0-9A-Fa-f]+;)/',
137
- '&amp;',
138
- $content
187
+ return self::processCDataAwareParts(
188
+ $content,
189
+ fn($part) => preg_replace(
190
+ '/&(?![a-zA-Z][A-Za-z0-9]*;|#[0-9]+;|#x[0-9A-Fa-f]+;)/',
191
+ '&amp;',
192
+ $part
193
+ )
139
194
  );
140
195
  }
141
196
 
142
197
  private static function escapeAttributeAngles(string $html): string
143
198
  {
144
199
  return preg_replace_callback(
145
- '/(\s[\w:-]+=)([\'"])(.*?)\2/s',
146
- 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],
147
203
  $html
148
204
  );
149
205
  }
150
206
 
151
- private static function escapeMustacheAngles(string $content): string
207
+ private static function escapeLiteralTextContent(string $content): string
152
208
  {
209
+ $literalTags = implode('|', array_keys(self::LITERAL_TEXT_TAGS));
210
+ $pattern = '/(<(?:' . $literalTags . ')\b[^>]*>)(.*?)(<\/(?:' . $literalTags . ')>)/is';
211
+
153
212
  return preg_replace_callback(
154
- '/\{\{[\s\S]*?\}\}/u',
155
- 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
+ },
156
231
  $content
157
232
  );
158
233
  }
159
234
 
160
- public static function convertToXml(string $templateContent): DOMDocument
235
+ private static function escapeMustacheAngles(string $content): string
161
236
  {
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;
237
+ return preg_replace_callback(
238
+ self::MUSTACHE_REGEX,
239
+ static fn($m) => str_replace(['<', '>'], ['&lt;', '&gt;'], $m[0]),
240
+ $content
241
+ );
181
242
  }
182
243
 
183
244
  private static function normalizeNamedEntities(string $html): string
184
245
  {
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');
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
+ }
189
268
 
190
- if ($decoded === $m[0]) {
191
- return $m[0];
192
- }
269
+ private static function processCDataAwareParts(string $content, callable $processor): string
270
+ {
271
+ $parts = preg_split('/(<!\[CDATA\[[\s\S]*?\]\]>)/', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
272
+ if ($parts === false) {
273
+ return $content;
274
+ }
193
275
 
194
- if (function_exists('mb_ord')) {
195
- return '&#' . mb_ord($decoded, 'UTF-8') . ';';
196
- }
276
+ foreach ($parts as $i => $part) {
277
+ if (!str_starts_with($part, '<![CDATA[')) {
278
+ $parts[$i] = $processor($part);
279
+ }
280
+ }
197
281
 
198
- $code = unpack('N', mb_convert_encoding($decoded, 'UCS-4BE', 'UTF-8'))[1];
199
- return '&#' . $code . ';';
200
- },
201
- $html
202
- );
282
+ return implode('', $parts);
203
283
  }
204
284
 
205
285
  private static function protectInlineScripts(string $html): string
206
286
  {
207
- if (stripos($html, '<script') === false) {
287
+ if (!str_contains($html, '<script')) {
208
288
  return $html;
209
289
  }
210
290
 
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
- );
291
+ $callback = static function (array $m): string {
292
+ if (preg_match('/\bsrc\s*=/i', $m[1])) {
293
+ return $m[0];
294
+ }
248
295
 
249
- if ($result === null) {
250
- $result = preg_replace_callback(
251
- '#<script\b([^>]*?)>(.*?)</script>#is',
252
- $callback,
253
- $content
254
- );
296
+ if (str_contains($m[2], '<![CDATA[')) {
297
+ return $m[0];
298
+ }
255
299
 
256
- return $result ?? $content;
300
+ $type = self::extractScriptType($m[1]);
301
+ if (!isset(self::SCRIPT_TYPES[$type])) {
302
+ return $m[0];
257
303
  }
258
304
 
259
- return $result;
305
+ $code = str_replace(']]>', ']]]]><![CDATA[>', $m[2]);
306
+ return "<script{$m[1]}><![CDATA[\n{$code}\n]]></script>";
260
307
  };
261
308
 
262
309
  if (preg_match('/^(.*?<body\b[^>]*>)(.*?)(<\/body>.*)$/is', $html, $parts)) {
263
- [$all, $beforeBody, $body, $afterBody] = $parts;
264
- return $beforeBody . $processScripts($body) . $afterBody;
310
+ [, $beforeBody, $body, $afterBody] = $parts;
311
+ return $beforeBody . self::processScriptsInContent($body, $callback) . $afterBody;
265
312
  }
266
313
 
267
- return $processScripts($html);
314
+ return self::processScriptsInContent($html, $callback);
268
315
  }
269
316
 
270
- public static function innerXml(DOMNode $node): string
317
+ private static function extractScriptType(string $attributes): string
271
318
  {
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);
319
+ if (preg_match('/\btype\s*=\s*([\'"]?)([^\'"\s>]+)/i', $attributes, $matches)) {
320
+ return strtolower($matches[2]);
282
321
  }
283
- return $html;
322
+ return '';
284
323
  }
285
324
 
286
- protected static function getXmlErrors(): array
325
+ private static function processScriptsInContent(string $content, callable $callback): string
287
326
  {
288
- $errors = libxml_get_errors();
289
- libxml_clear_errors();
290
- return array_map(fn($e) => self::formatLibxmlError($e), $errors);
327
+ return preg_replace_callback(self::SCRIPT_REGEX, $callback, $content) ?? $content;
291
328
  }
292
329
 
293
- protected static function formatLibxmlError(LibXMLError $error): string
330
+ protected static function processNode(DOMNode $node): string
294
331
  {
295
- $type = match ($error->level) {
296
- LIBXML_ERR_WARNING => 'Warning',
297
- LIBXML_ERR_ERROR => 'Error',
298
- LIBXML_ERR_FATAL => 'Fatal',
299
- 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,
300
337
  };
301
- return sprintf(
302
- "[%s] Line %d, Col %d: %s",
303
- $type,
304
- $error->line,
305
- $error->column,
306
- trim($error->message)
307
- );
308
338
  }
309
339
 
310
- protected static function processNode(DOMNode $node): string
340
+ private static function processElementNode(DOMElement $node): string
311
341
  {
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);
342
+ $tag = strtolower($node->nodeName);
343
+ $pushed = false;
319
344
 
320
- if (
321
- $tag === 'script' &&
322
- !$node->hasAttribute('src') &&
323
- !$node->hasAttribute('type')
324
- ) {
325
- $node->setAttribute('type', 'text/php');
326
- }
345
+ if ($tag === 'script' && !$node->hasAttribute('src') && !$node->hasAttribute('type')) {
346
+ $node->setAttribute('type', 'text/php');
347
+ }
327
348
 
328
- if ($node->hasAttribute('pp-component')) {
329
- self::$sectionStack[] = $node->getAttribute('pp-component');
330
- $pushed = true;
331
- }
349
+ // Handle component sections
350
+ if ($node->hasAttribute('pp-component')) {
351
+ self::$sectionStack[] = $node->getAttribute('pp-component');
352
+ $pushed = true;
353
+ }
332
354
 
355
+ try {
333
356
  self::processAttributes($node);
334
357
 
335
358
  if (isset(self::$classMappings[$node->nodeName])) {
336
- $html = self::renderComponent(
359
+ return self::renderComponent(
337
360
  $node,
338
361
  $node->nodeName,
339
362
  self::getNodeAttributes($node)
340
363
  );
341
- if ($pushed) {
342
- array_pop(self::$sectionStack);
343
- }
344
- return $html;
345
364
  }
346
365
 
347
- $children = '';
348
- foreach ($node->childNodes as $c) {
349
- $children .= self::processNode($c);
350
- }
366
+ $children = implode('', self::processChildNodes($node->childNodes));
351
367
  $attrs = self::getNodeAttributes($node) + ['children' => $children];
352
- $out = self::renderAsHtml($node->nodeName, $attrs);
353
368
 
369
+ return self::renderAsHtml($node->nodeName, $attrs);
370
+ } finally {
354
371
  if ($pushed) {
355
372
  array_pop(self::$sectionStack);
356
373
  }
357
- return $out;
358
- }
359
-
360
- if ($node instanceof DOMComment) {
361
- return "<!--{$node->textContent}-->";
362
374
  }
363
-
364
- return $node->textContent;
365
375
  }
366
376
 
367
377
  private static function processTextNode(DOMText $node): string
368
378
  {
369
379
  $parent = strtolower($node->parentNode?->nodeName ?? '');
380
+
370
381
  if (isset(self::LITERAL_TEXT_TAGS[$parent])) {
371
382
  return htmlspecialchars(
372
383
  $node->textContent,
@@ -377,32 +388,17 @@ class TemplateCompiler
377
388
 
378
389
  return preg_replace_callback(
379
390
  self::BINDING_REGEX,
380
- fn($m) => self::processBindingExpression(trim($m[1])),
391
+ static fn($m) => self::processBindingExpression(trim($m[1])),
381
392
  $node->textContent
382
393
  );
383
394
  }
384
395
 
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
396
  private static function processBindingExpression(string $expr): string
398
397
  {
399
398
  $escaped = htmlspecialchars($expr, ENT_QUOTES, 'UTF-8');
399
+ $attribute = preg_match('/^[\w.]+$/u', $expr) ? 'pp-bind' : 'pp-bind-expr';
400
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>";
401
+ return "<span {$attribute}=\"{$escaped}\"></span>";
406
402
  }
407
403
 
408
404
  protected static function renderComponent(
@@ -410,232 +406,284 @@ class TemplateCompiler
410
406
  string $componentName,
411
407
  array $incomingProps
412
408
  ): 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}";
409
+ $mapping = self::selectComponentMapping($componentName);
410
+ $sectionId = self::generateSectionId($mapping['className']);
419
411
 
420
412
  $originalStack = self::$sectionStack;
421
413
  self::$sectionStack[] = $sectionId;
422
414
 
423
- PHPX::setRenderingContext($originalStack, $sectionId);
415
+ try {
416
+ PHPX::setRenderingContext($originalStack, $sectionId);
417
+
418
+ $instance = self::initializeComponentInstance($mapping, $incomingProps);
419
+ $instance->children = self::getChildrenHtml($node);
424
420
 
425
- $instance = self::initializeComponentInstance($mapping, $incomingProps);
421
+ PHPX::setRenderingContext($originalStack, $sectionId);
426
422
 
427
- $childHtml = '';
428
- foreach ($node->childNodes as $c) {
429
- $childHtml .= self::processNode($c);
423
+ return self::compileComponentHtml($instance->render(), $sectionId);
424
+ } finally {
425
+ self::$sectionStack = $originalStack;
430
426
  }
427
+ }
431
428
 
432
- 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;
433
434
 
434
- $instance->children = trim($childHtml);
435
+ return $idx === 0 ? $baseId : "{$baseId}{$idx}";
436
+ }
435
437
 
436
- 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
+ }
437
446
 
438
- $html = $instance->render();
447
+ private static function compileComponentHtml(string $html, string $sectionId): string
448
+ {
439
449
  $html = self::preprocessFragmentSyntax($html);
440
-
441
450
  $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);
451
+
452
+ foreach ($fragDom->documentElement->childNodes as $child) {
453
+ if ($child instanceof DOMElement) {
454
+ $child->setAttribute('pp-component', $sectionId);
446
455
  break;
447
456
  }
448
457
  }
449
458
 
450
459
  $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
- );
460
+ $htmlOut = self::normalizeSelfClosingTags($htmlOut);
458
461
 
459
- if (
460
- str_contains($htmlOut, '{{') ||
461
- self::hasComponentTag($htmlOut) ||
462
- stripos($htmlOut, '<script') !== false
463
- ) {
462
+ if (self::needsRecompilation($htmlOut)) {
464
463
  $htmlOut = self::compile($htmlOut);
465
464
  }
466
465
 
467
466
  return $htmlOut;
468
467
  }
469
468
 
470
- private static function preprocessFragmentSyntax(string $content): string
469
+ private static function needsRecompilation(string $html): bool
471
470
  {
472
- $content = preg_replace('/<>/', '<Fragment>', $content);
473
- $content = preg_replace('/<\/>/', '</Fragment>', $content);
474
-
475
- return $content;
471
+ return str_contains($html, '{{') ||
472
+ preg_match(self::COMPONENT_TAG_REGEX, $html) === 1 ||
473
+ stripos($html, '<script') !== false;
476
474
  }
477
475
 
478
- private static function selectComponentMapping(string $componentName): array
476
+ private static function normalizeSelfClosingTags(string $html): string
479
477
  {
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;
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
+ );
500
485
  }
501
486
 
502
- protected static function initializeComponentInstance(array $mapping, array $attributes)
487
+ private static function initializeComponentInstance(array $mapping, array $attributes)
503
488
  {
504
- if (!isset($mapping['className'], $mapping['filePath'])) {
505
- throw new RuntimeException("Invalid mapping");
506
- }
489
+ ['className' => $className, 'filePath' => $filePath] = $mapping;
507
490
 
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
- }
491
+ self::ensureClassLoaded($className, $filePath);
492
+ $reflection = self::getClassReflection($className);
515
493
 
516
- self::cacheClassReflection($className);
494
+ self::validateComponentProps($className, $attributes);
517
495
 
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
- }
496
+ $instance = $reflection['class']->newInstanceWithoutConstructor();
497
+ self::setInstanceProperties($instance, $attributes, $reflection['properties']);
527
498
 
528
- self::validateComponentProps($className, $attributes);
499
+ $reflection['constructor']?->invoke($instance, $attributes);
529
500
 
530
- $ref = self::$reflections[$className];
531
- $ctor = self::$constructors[$className];
532
- $inst = $ref->newInstanceWithoutConstructor();
501
+ return $instance;
502
+ }
533
503
 
534
- foreach (self::$publicProperties[$className] as $prop) {
535
- $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);
536
508
 
537
- if (!array_key_exists($name, $attributes)) {
538
- continue;
509
+ if (!class_exists($className)) {
510
+ throw new RuntimeException("Class {$className} not found");
539
511
  }
540
- $value = self::coerce($attributes[$name], $prop->getType());
541
- $prop->setValue($inst, $value);
542
512
  }
543
-
544
- if ($ctor) {
545
- $ctor->invoke($inst, $attributes);
546
- }
547
-
548
- return $inst;
549
513
  }
550
514
 
551
- private static function cacheClassReflection(string $className): void
515
+ private static function getClassReflection(string $className): array
552
516
  {
553
- if (isset(self::$reflections[$className])) {
554
- return;
555
- }
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
+ );
556
523
 
557
- $rc = new ReflectionClass($className);
558
- self::$reflections[$className] = $rc;
559
- 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
+ }
560
533
 
561
- $publicProps = array_filter(
562
- $rc->getProperties(ReflectionProperty::IS_PUBLIC),
563
- fn(ReflectionProperty $p) => !$p->isStatic()
564
- );
565
- self::$publicProperties[$className] = $publicProps;
534
+ return self::$reflectionCache[$className];
535
+ }
566
536
 
567
- $allowed = self::SYSTEM_PROPS;
568
- foreach ($publicProps as $prop) {
569
- $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
+ }
570
548
  }
571
- self::$allowedProps[$className] = $allowed;
572
549
  }
573
550
 
574
551
  private static function validateComponentProps(string $className, array $attributes): void
575
552
  {
576
- foreach (self::$publicProperties[$className] as $prop) {
553
+ $reflection = self::getClassReflection($className);
554
+
555
+ foreach ($reflection['properties'] as $prop) {
577
556
  $name = $prop->getName();
578
557
  $type = $prop->getType();
579
558
 
580
559
  if (
581
- $type instanceof ReflectionNamedType && $type->isBuiltin()
582
- && ! $type->allowsNull()
583
- && ! array_key_exists($name, $attributes)
560
+ $type instanceof ReflectionNamedType &&
561
+ $type->isBuiltin() &&
562
+ !$type->allowsNull() &&
563
+ !array_key_exists($name, $attributes)
584
564
  ) {
585
565
  throw new ComponentValidationException(
586
566
  $name,
587
567
  $className,
588
- array_map(fn($p) => $p->getName(), self::$publicProperties[$className])
568
+ array_map(static fn($p) => $p->getName(), $reflection['properties'])
589
569
  );
590
570
  }
591
571
  }
592
-
593
- return;
594
572
  }
595
573
 
596
- private static function coerce(mixed $value, ?ReflectionType $type): mixed
574
+ private static function selectComponentMapping(string $componentName): array
597
575
  {
598
- 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];
599
599
  }
600
600
 
601
- protected static function initializeClassMappings(): void
601
+ public static function innerXml(DOMNode $node): string
602
602
  {
603
- foreach (PrismaPHPSettings::$classLogFiles as $tag => $cls) {
604
- 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);
605
610
  }
611
+
612
+ return $html;
613
+ }
614
+
615
+ private static function preprocessFragmentSyntax(string $content): string
616
+ {
617
+ return str_replace(['<>', '</>'], ['<Fragment>', '</Fragment>'], $content);
606
618
  }
607
619
 
608
- protected static function hasComponentTag(string $html): bool
620
+ private static function processAttributes(DOMElement $node): void
609
621
  {
610
- 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
+ }
611
627
  }
612
628
 
613
629
  private static function getNodeAttributes(DOMElement $node): array
614
630
  {
615
- $out = [];
616
- foreach ($node->attributes as $a) {
617
- $out[$a->name] = $a->value;
631
+ $attrs = [];
632
+ foreach ($node->attributes as $attr) {
633
+ $attrs[$attr->name] = $attr->value;
618
634
  }
619
- return $out;
635
+ return $attrs;
620
636
  }
621
637
 
622
638
  private static function renderAsHtml(string $tag, array $attrs): string
623
639
  {
624
- $pairs = [];
625
- foreach ($attrs as $k => $v) {
626
- if ($k === 'children') {
627
- 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
+ );
628
653
  }
629
- $pairs[] = sprintf(
630
- '%s="%s"',
631
- $k,
632
- htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
633
- );
654
+ $attrStr = ' ' . implode(' ', $pairs);
634
655
  }
635
- $attrStr = $pairs ? ' ' . implode(' ', $pairs) : '';
636
656
 
637
- return in_array(strtolower($tag), self::$selfClosingTags, true)
657
+ return isset(self::SELF_CLOSING_TAGS[strtolower($tag)])
638
658
  ? "<{$tag}{$attrStr} />"
639
- : "<{$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);
640
688
  }
641
689
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-prisma-php-app",
3
- "version": "3.6.2",
3
+ "version": "3.6.4",
4
4
  "description": "Prisma-PHP: A Revolutionary Library Bridging PHP with Prisma ORM",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",