create-prisma-php-app 3.0.3 → 3.1.0

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.
@@ -22,7 +22,7 @@ use Lib\ErrorHandler;
22
22
  use Firebase\JWT\JWT;
23
23
  use Firebase\JWT\Key;
24
24
 
25
- final class Bootstrap
25
+ final class Bootstrap extends RuntimeException
26
26
  {
27
27
  public static string $contentToInclude = '';
28
28
  public static array $layoutsToInclude = [];
@@ -35,12 +35,22 @@ final class Bootstrap
35
35
  public static bool $secondRequestC69CD = false;
36
36
  public static array $requestFilesData = [];
37
37
 
38
+ private string $context;
39
+
38
40
  private static array $fileExistCache = [];
39
41
  private static array $regexCache = [];
40
42
 
41
- /**
42
- * Main entry point to run the entire routing and rendering logic.
43
- */
43
+ public function __construct(string $message, string $context = '', int $code = 0, ?Throwable $previous = null)
44
+ {
45
+ $this->context = $context;
46
+ parent::__construct($message, $code, $previous);
47
+ }
48
+
49
+ public function getContext(): string
50
+ {
51
+ return $this->context;
52
+ }
53
+
44
54
  public static function run(): void
45
55
  {
46
56
  // Load environment variables
@@ -1025,15 +1035,18 @@ try {
1025
1035
  }
1026
1036
  } catch (Throwable $e) {
1027
1037
  if (Bootstrap::isAjaxOrXFileRequestOrRouteFile()) {
1028
- $errorDetails = "Unhandled Exception: " . $e->getMessage() .
1029
- " in " . $e->getFile() .
1030
- " on line " . $e->getLine();
1038
+ $errorDetails = json_encode([
1039
+ 'success' => false,
1040
+ 'error' => [
1041
+ 'type' => get_class($e),
1042
+ 'message' => $e->getMessage(),
1043
+ 'file' => $e->getFile(),
1044
+ 'line' => $e->getLine(),
1045
+ 'trace' => $e->getTraceAsString()
1046
+ ]
1047
+ ]);
1031
1048
  } else {
1032
- $errorDetails = "Unhandled Exception: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
1033
- $errorDetails .= "<br>File: " . htmlspecialchars($e->getFile(), ENT_QUOTES, 'UTF-8');
1034
- $errorDetails .= "<br>Line: " . htmlspecialchars((string)$e->getLine(), ENT_QUOTES, 'UTF-8');
1035
- $errorDetails .= "<br/>TraceAsString: " . htmlspecialchars($e->getTraceAsString(), ENT_QUOTES, 'UTF-8');
1036
- $errorDetails = "<div class='error'>{$errorDetails}</div>";
1049
+ $errorDetails = ErrorHandler::formatExceptionForDisplay($e);
1037
1050
  }
1038
1051
  ErrorHandler::modifyOutputLayoutForError($errorDetails);
1039
1052
  }
@@ -4,6 +4,8 @@ namespace Lib;
4
4
 
5
5
  use Bootstrap;
6
6
  use Lib\MainLayout;
7
+ use Throwable;
8
+ use Lib\PHPX\Exceptions\ComponentValidationException;
7
9
 
8
10
  class ErrorHandler
9
11
  {
@@ -123,4 +125,196 @@ class ErrorHandler
123
125
  }
124
126
  exit;
125
127
  }
128
+
129
+ public static function formatExceptionForDisplay(Throwable $exception): string
130
+ {
131
+ // Handle specific exception types
132
+ if ($exception instanceof ComponentValidationException) {
133
+ return self::formatComponentValidationError($exception);
134
+ }
135
+
136
+ // Handle template compilation errors specifically
137
+ if (strpos($exception->getMessage(), 'Invalid prop') !== false) {
138
+ return self::formatTemplateCompilerError($exception);
139
+ }
140
+
141
+ // Generic exception formatting
142
+ return self::formatGenericException($exception);
143
+ }
144
+
145
+ private static function formatComponentValidationError(ComponentValidationException $exception): string
146
+ {
147
+ $message = htmlspecialchars($exception->getMessage(), ENT_QUOTES, 'UTF-8');
148
+ $file = htmlspecialchars($exception->getFile(), ENT_QUOTES, 'UTF-8');
149
+ $line = $exception->getLine();
150
+
151
+ // Get the details from the ComponentValidationException
152
+ $propName = method_exists($exception, 'getPropName') ? $exception->getPropName() : 'unknown';
153
+ $componentName = method_exists($exception, 'getComponentName') ? $exception->getComponentName() : 'unknown';
154
+ $availableProps = method_exists($exception, 'getAvailableProps') ? $exception->getAvailableProps() : [];
155
+
156
+ $availablePropsString = !empty($availableProps) ? implode(', ', $availableProps) : 'none defined';
157
+
158
+ return <<<HTML
159
+ <div class="error-container max-w-4xl mx-auto mt-8 bg-red-50 border border-red-200 rounded-lg shadow-lg">
160
+ <div class="bg-red-100 px-6 py-4 border-b border-red-200">
161
+ <h2 class="text-xl font-bold text-red-800 flex items-center">
162
+ <svg class="w-6 h-6 mr-2" fill="currentColor" viewBox="0 0 20 20">
163
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
164
+ </svg>
165
+ Component Validation Error
166
+ </h2>
167
+ </div>
168
+
169
+ <div class="p-6">
170
+ <div class="bg-white border border-red-200 rounded-lg p-4 mb-4">
171
+ <div class="mb-3">
172
+ <span class="inline-block bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-medium">
173
+ Component: {$componentName}
174
+ </span>
175
+ <span class="inline-block bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-medium ml-2">
176
+ Invalid Prop: {$propName}
177
+ </span>
178
+ </div>
179
+ <pre class="text-sm text-red-800 whitespace-pre-wrap font-mono">{$message}</pre>
180
+ </div>
181
+
182
+ <div class="text-sm text-gray-600 mb-4">
183
+ <strong>File:</strong> <code class="bg-gray-100 px-2 py-1 rounded text-xs">{$file}</code><br />
184
+ <strong>Line:</strong> <span class="bg-gray-100 px-2 py-1 rounded text-xs">{$line}</span>
185
+ </div>
186
+
187
+ <div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
188
+ <h4 class="font-medium text-blue-800 mb-2">💡 Available Props:</h4>
189
+ <p class="text-blue-700 text-sm">
190
+ <code class="bg-blue-100 px-2 py-1 rounded text-xs">{$availablePropsString}</code>
191
+ </p>
192
+ </div>
193
+
194
+ <div class="bg-green-50 border border-green-200 rounded-lg p-4 mb-4">
195
+ <h4 class="font-medium text-green-800 mb-2">🔧 Quick Fixes:</h4>
196
+ <ul class="text-green-700 text-sm space-y-1">
197
+ <li>• Remove the '<code>{$propName}</code>' prop from your template</li>
198
+ <li>• Add '<code>public \${$propName};</code>' to your <code>{$componentName}</code> component class</li>
199
+ <li>• Use data attributes: '<code>data-{$propName}</code>' instead</li>
200
+ </ul>
201
+ </div>
202
+
203
+ <details class="mt-4">
204
+ <summary class="cursor-pointer text-red-600 font-medium hover:text-red-800 select-none">
205
+ Show Stack Trace
206
+ </summary>
207
+ <div class="mt-3 bg-gray-50 border border-gray-200 rounded p-4">
208
+ <pre class="text-xs text-gray-700 overflow-auto whitespace-pre-wrap max-h-96">{$exception->getTraceAsString()}</pre>
209
+ </div>
210
+ </details>
211
+ </div>
212
+ </div>
213
+ HTML;
214
+ }
215
+
216
+ private static function formatTemplateCompilerError(Throwable $exception): string
217
+ {
218
+ $message = htmlspecialchars($exception->getMessage(), ENT_QUOTES, 'UTF-8');
219
+ $file = htmlspecialchars($exception->getFile(), ENT_QUOTES, 'UTF-8');
220
+ $line = $exception->getLine();
221
+
222
+ // Extract the component validation error details
223
+ if (preg_match("/Invalid prop '([^']+)' passed to component '([^']+)'/", $exception->getMessage(), $matches)) {
224
+ $invalidProp = $matches[1];
225
+ $componentName = $matches[2];
226
+
227
+ return <<<HTML
228
+ <div class="error-container max-w-4xl mx-auto mt-8 bg-red-50 border border-red-200 rounded-lg shadow-lg">
229
+ <div class="bg-red-100 px-6 py-4 border-b border-red-200">
230
+ <h2 class="text-xl font-bold text-red-800 flex items-center">
231
+ <svg class="w-6 h-6 mr-2" fill="currentColor" viewBox="0 0 20 20">
232
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
233
+ </svg>
234
+ Template Compilation Error
235
+ </h2>
236
+ </div>
237
+
238
+ <div class="p-6">
239
+ <div class="bg-white border border-red-200 rounded-lg p-4 mb-4">
240
+ <div class="mb-3">
241
+ <span class="inline-block bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-medium">
242
+ Component: {$componentName}
243
+ </span>
244
+ <span class="inline-block bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-medium ml-2">
245
+ Invalid Prop: {$invalidProp}
246
+ </span>
247
+ </div>
248
+ <p class="text-red-800 font-medium">{$message}</p>
249
+ </div>
250
+
251
+ <div class="text-sm text-gray-600 mb-4">
252
+ <strong>File:</strong> {$file}<br />
253
+ <strong>Line:</strong> {$line}
254
+ </div>
255
+
256
+ <div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
257
+ <h4 class="font-medium text-blue-800 mb-2">💡 Quick Fix:</h4>
258
+ <p class="text-blue-700 text-sm">
259
+ Either remove the '<code>{$invalidProp}</code>' prop from your template, or add it as a public property to your <code>{$componentName}</code> component class.
260
+ </p>
261
+ </div>
262
+
263
+ <details class="mt-4">
264
+ <summary class="cursor-pointer text-red-600 font-medium hover:text-red-800 select-none">
265
+ Show Stack Trace
266
+ </summary>
267
+ <div class="mt-3 bg-gray-50 border border-gray-200 rounded p-4">
268
+ <pre class="text-xs text-gray-700 overflow-auto whitespace-pre-wrap">{$exception->getTraceAsString()}</pre>
269
+ </div>
270
+ </details>
271
+ </div>
272
+ </div>
273
+ HTML;
274
+ }
275
+
276
+ // Fallback to generic formatting
277
+ return self::formatGenericException($exception);
278
+ }
279
+
280
+ private static function formatGenericException(Throwable $exception): string
281
+ {
282
+ $type = htmlspecialchars(get_class($exception), ENT_QUOTES, 'UTF-8');
283
+ $message = htmlspecialchars($exception->getMessage(), ENT_QUOTES, 'UTF-8');
284
+ $file = htmlspecialchars($exception->getFile(), ENT_QUOTES, 'UTF-8');
285
+ $line = $exception->getLine();
286
+
287
+ return <<<HTML
288
+ <div class="error-container max-w-4xl mx-auto mt-8 bg-red-50 border border-red-200 rounded-lg shadow-lg">
289
+ <div class="bg-red-100 px-6 py-4 border-b border-red-200">
290
+ <h2 class="text-xl font-bold text-red-800 flex items-center">
291
+ <svg class="w-6 h-6 mr-2" fill="currentColor" viewBox="0 0 20 20">
292
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
293
+ </svg>
294
+ {$type}
295
+ </h2>
296
+ </div>
297
+
298
+ <div class="p-6">
299
+ <div class="bg-white border border-red-200 rounded-lg p-4 mb-4">
300
+ <p class="text-red-800 font-medium break-words">{$message}</p>
301
+ </div>
302
+
303
+ <div class="text-sm text-gray-600 mb-4">
304
+ <strong>File:</strong> <code class="bg-gray-100 px-2 py-1 rounded text-xs">{$file}</code><br />
305
+ <strong>Line:</strong> <span class="bg-gray-100 px-2 py-1 rounded text-xs">{$line}</span>
306
+ </div>
307
+
308
+ <details class="mt-4">
309
+ <summary class="cursor-pointer text-red-600 font-medium hover:text-red-800 select-none">
310
+ Show Stack Trace
311
+ </summary>
312
+ <div class="mt-3 bg-gray-50 border border-gray-200 rounded p-4">
313
+ <pre class="text-xs text-gray-700 overflow-auto whitespace-pre-wrap max-h-96">{$exception->getTraceAsString()}</pre>
314
+ </div>
315
+ </details>
316
+ </div>
317
+ </div>
318
+ HTML;
319
+ }
126
320
  }
@@ -4,10 +4,6 @@ namespace Lib;
4
4
 
5
5
  use RuntimeException;
6
6
  use InvalidArgumentException;
7
- use Lib\PrismaPHPSettings;
8
- use DOMDocument;
9
- use DOMElement;
10
- use DOMXPath;
11
7
  use Lib\PHPX\TemplateCompiler;
12
8
 
13
9
  class IncludeTracker
@@ -15,7 +11,7 @@ class IncludeTracker
15
11
  public static array $sections = [];
16
12
 
17
13
  /**
18
- * Includes and echoes a file wrapped in a unique pp-section-id container.
14
+ * Includes and echoes a file wrapped in a unique pp-component container.
19
15
  * Supported $mode values: 'include', 'include_once', 'require', 'require_once'
20
16
  *
21
17
  * @param string $filePath The path to the file to be included.
@@ -43,8 +39,6 @@ class IncludeTracker
43
39
  $wrapped = self::wrapWithId($filePath, $html);
44
40
  $fragDom = TemplateCompiler::convertToXml($wrapped);
45
41
 
46
- self::prefixInlineHandlers($fragDom);
47
-
48
42
  $newHtml = TemplateCompiler::innerXml($fragDom);
49
43
 
50
44
  self::$sections[$filePath] = [
@@ -58,39 +52,6 @@ class IncludeTracker
58
52
  private static function wrapWithId(string $filePath, string $html): string
59
53
  {
60
54
  $id = 's' . base_convert(sprintf('%u', crc32($filePath)), 10, 36);
61
- return "<div pp-section-id=\"$id\">\n$html\n</div>";
62
- }
63
-
64
- private static function prefixInlineHandlers(DOMDocument $doc): void
65
- {
66
- $xp = new DOMXPath($doc);
67
-
68
- /** @var DOMElement $el */
69
- foreach ($xp->query('//*') as $el) {
70
- $handlers = [];
71
-
72
- foreach (iterator_to_array($el->attributes) as $attr) {
73
- $name = $attr->name;
74
-
75
- if (!str_starts_with($name, 'on')) {
76
- continue;
77
- }
78
-
79
- $event = substr($name, 2);
80
- if (
81
- !in_array($event, PrismaPHPSettings::$htmlEvents, true) ||
82
- trim($attr->value) === ''
83
- ) {
84
- continue;
85
- }
86
-
87
- $handlers[$name] = $attr->value;
88
- $el->removeAttribute($name);
89
- }
90
-
91
- foreach ($handlers as $origName => $value) {
92
- $el->setAttribute("pp-inc-{$origName}", $value);
93
- }
94
- }
55
+ return "<div pp-component=\"$id\">\n$html\n</div>";
95
56
  }
96
57
  }
@@ -0,0 +1,49 @@
1
+ <?php
2
+
3
+ namespace Lib\PHPX\Exceptions;
4
+
5
+ use RuntimeException;
6
+
7
+ class ComponentValidationException extends RuntimeException
8
+ {
9
+ private string $propName;
10
+ private string $componentName;
11
+ private array $availableProps;
12
+
13
+ public function __construct(
14
+ string $propName,
15
+ string $componentName,
16
+ array $availableProps,
17
+ string $context = ''
18
+ ) {
19
+ $this->propName = $propName;
20
+ $this->componentName = $componentName;
21
+ $this->availableProps = $availableProps;
22
+
23
+ $availableList = implode(', ', $availableProps);
24
+
25
+ $message = "Invalid prop '{$propName}' for component '{$componentName}'.\n";
26
+ $message .= "Available props: {$availableList}";
27
+
28
+ if ($context) {
29
+ $message .= "\n{$context}";
30
+ }
31
+
32
+ parent::__construct($message);
33
+ }
34
+
35
+ public function getPropName(): string
36
+ {
37
+ return $this->propName;
38
+ }
39
+
40
+ public function getComponentName(): string
41
+ {
42
+ return $this->componentName;
43
+ }
44
+
45
+ public function getAvailableProps(): array
46
+ {
47
+ return $this->availableProps;
48
+ }
49
+ }
@@ -19,11 +19,6 @@ class PHPX implements IPHPX
19
19
  */
20
20
  public mixed $children;
21
21
 
22
- /**
23
- * @var string The CSS class for custom styling.
24
- */
25
- protected string $class;
26
-
27
22
  /**
28
23
  * @var array<string, mixed> The array representation of the HTML attributes.
29
24
  */
@@ -38,27 +33,27 @@ class PHPX implements IPHPX
38
33
  {
39
34
  $this->props = $props;
40
35
  $this->children = $props['children'] ?? '';
41
- $this->class = $props['class'] ?? '';
42
36
  }
43
37
 
44
38
  /**
45
39
  * Combines and returns the CSS classes for the component.
46
40
  *
47
41
  * This method merges the provided classes, which can be either strings or arrays of strings,
48
- * with the component's `$class` property. It uses the `Utils::mergeClasses` method to ensure
49
- * that the resulting CSS class string is optimized, with duplicate or conflicting classes removed.
42
+ * without automatically including the component's `$class` property. It uses the `Utils::mergeClasses`
43
+ * method to ensure that the resulting CSS class string is optimized, with duplicate or conflicting
44
+ * classes removed.
50
45
  *
51
46
  * ### Features:
52
47
  * - Accepts multiple arguments as strings or arrays of strings.
53
- * - Automatically merges the provided classes with `$this->class`.
48
+ * - Only merges the classes provided as arguments (does not include `$this->class` automatically).
54
49
  * - Ensures the final CSS class string is well-formatted and free of conflicts.
55
50
  *
56
51
  * @param string|array ...$classes The CSS classes to be merged. Each argument can be a string or an array of strings.
57
- * @return string A single CSS class string with the merged and optimized classes, including `$this->class`.
52
+ * @return string A single CSS class string with the merged and optimized classes.
58
53
  */
59
54
  protected function getMergeClasses(string|array ...$classes): string
60
55
  {
61
- $all = array_merge($classes, [$this->class]);
56
+ $all = array_merge($classes);
62
57
 
63
58
  $expr = [];
64
59
  foreach ($all as &$chunk) {
@@ -14,11 +14,11 @@ use DOMText;
14
14
  use RuntimeException;
15
15
  use Bootstrap;
16
16
  use LibXMLError;
17
- use DOMXPath;
18
17
  use ReflectionClass;
19
18
  use ReflectionProperty;
20
19
  use ReflectionType;
21
20
  use ReflectionNamedType;
21
+ use Lib\PHPX\Exceptions\ComponentValidationException;
22
22
 
23
23
  class TemplateCompiler
24
24
  {
@@ -30,6 +30,11 @@ class TemplateCompiler
30
30
  'kbd' => true,
31
31
  'var' => true,
32
32
  ];
33
+ private const SYSTEM_PROPS = [
34
+ 'children' => true,
35
+ 'key' => true,
36
+ 'ref' => true,
37
+ ];
33
38
 
34
39
  protected static array $classMappings = [];
35
40
  protected static array $selfClosingTags = [
@@ -56,6 +61,7 @@ class TemplateCompiler
56
61
  private static array $reflections = [];
57
62
  private static array $constructors = [];
58
63
  private static array $publicProperties = [];
64
+ private static array $allowedProps = [];
59
65
 
60
66
  public static function compile(string $templateContent): string
61
67
  {
@@ -276,8 +282,8 @@ class TemplateCompiler
276
282
  $node->setAttribute('type', 'text/php');
277
283
  }
278
284
 
279
- if ($node->hasAttribute('pp-section-id')) {
280
- self::$sectionStack[] = $node->getAttribute('pp-section-id');
285
+ if ($node->hasAttribute('pp-component')) {
286
+ self::$sectionStack[] = $node->getAttribute('pp-component');
281
287
  $pushed = true;
282
288
  }
283
289
 
@@ -361,7 +367,6 @@ class TemplateCompiler
361
367
  string $componentName,
362
368
  array $incomingProps
363
369
  ): string {
364
- $incomingProps = self::sanitizeIncomingProps($incomingProps);
365
370
  $mapping = self::selectComponentMapping($componentName);
366
371
  $instance = self::initializeComponentInstance($mapping, $incomingProps);
367
372
 
@@ -370,7 +375,7 @@ class TemplateCompiler
370
375
  $childHtml .= self::processNode($c);
371
376
  }
372
377
 
373
- $instance->children = self::sanitizeEventAttributes($childHtml);
378
+ $instance->children = $childHtml;
374
379
 
375
380
  $baseId = 's' . base_convert(sprintf('%u', crc32($mapping['className'])), 10, 36);
376
381
  $idx = self::$componentInstanceCounts[$baseId] ?? 0;
@@ -379,55 +384,10 @@ class TemplateCompiler
379
384
 
380
385
  $html = $instance->render();
381
386
  $fragDom = self::convertToXml($html);
382
- $xpath = new DOMXPath($fragDom);
383
-
384
- /** @var DOMElement $el */
385
- foreach ($xpath->query('//*') as $el) {
386
-
387
- $tag = $el->tagName;
388
- if (ctype_upper($tag[0]) || isset(self::$classMappings[$tag])) {
389
- continue;
390
- }
391
-
392
- $originalEvents = [];
393
- $componentEvents = [];
394
-
395
- foreach (iterator_to_array($el->attributes) as $attr) {
396
- $name = $attr->name;
397
- $value = $attr->value;
398
-
399
- if (str_starts_with($name, 'pp-original-')) {
400
- $origName = substr($name, strlen('pp-original-'));
401
- $originalEvents[$origName] = $value;
402
- } elseif (str_starts_with($name, 'on')) {
403
- $event = substr($name, 2);
404
- if ($value !== '' && in_array($event, PrismaPHPSettings::$htmlEvents, true)) {
405
- $componentEvents[$name] = $value;
406
- }
407
- }
408
- }
409
-
410
- foreach (array_keys($originalEvents) as $k) $el->removeAttribute("pp-original-{$k}");
411
- foreach (array_keys($componentEvents) as $k) $el->removeAttribute($k);
412
-
413
- foreach ($componentEvents as $evAttr => $compValue) {
414
- $el->setAttribute("data-pp-child-{$evAttr}", $compValue);
415
-
416
- if (isset($originalEvents[$evAttr])) {
417
- $el->setAttribute("data-pp-parent-{$evAttr}", $originalEvents[$evAttr]);
418
- unset($originalEvents[$evAttr]);
419
- }
420
- }
421
-
422
- foreach ($originalEvents as $name => $value) {
423
- $el->setAttribute($name, $value);
424
- }
425
- }
426
-
427
387
  $root = $fragDom->documentElement;
428
388
  foreach ($root->childNodes as $c) {
429
389
  if ($c instanceof DOMElement) {
430
- $c->setAttribute('pp-phpx-id', $sectionId);
390
+ $c->setAttribute('pp-component', $sectionId);
431
391
  break;
432
392
  }
433
393
  }
@@ -444,53 +404,6 @@ class TemplateCompiler
444
404
  return $htmlOut;
445
405
  }
446
406
 
447
- protected static function sanitizeIncomingProps(array $props): array
448
- {
449
- foreach ($props as $key => $val) {
450
- if (str_starts_with($key, 'on')) {
451
- $event = substr($key, 2);
452
- if (in_array($event, PrismaPHPSettings::$htmlEvents, true) && trim((string)$val) !== '') {
453
- $props["pp-original-on{$event}"] = (string)$val;
454
- unset($props[$key]);
455
- }
456
- }
457
- }
458
-
459
- return $props;
460
- }
461
-
462
- protected static function sanitizeEventAttributes(string $html): string
463
- {
464
- $fragDom = self::convertToXml($html, false);
465
- $xpath = new DOMXPath($fragDom);
466
-
467
- /** @var DOMElement $el */
468
- foreach ($xpath->query('//*') as $el) {
469
- foreach (iterator_to_array($el->attributes) as $attr) {
470
- $name = strtolower($attr->name);
471
-
472
- if (!str_starts_with($name, 'on')) {
473
- continue;
474
- }
475
-
476
- $event = substr($name, 2);
477
- $value = trim($attr->value);
478
-
479
- if ($value !== '' && in_array($event, PrismaPHPSettings::$htmlEvents, true)) {
480
- $el->setAttribute("pp-original-on{$event}", $value);
481
- }
482
-
483
- $el->removeAttribute($name);
484
- }
485
- }
486
-
487
- $body = $fragDom->getElementsByTagName('body')[0] ?? null;
488
-
489
- return $body instanceof DOMElement
490
- ? self::innerXml($body)
491
- : self::innerXml($fragDom);
492
- }
493
-
494
407
  private static function selectComponentMapping(string $componentName): array
495
408
  {
496
409
  if (!isset(self::$classMappings[$componentName])) {
@@ -529,16 +442,20 @@ class TemplateCompiler
529
442
  throw new RuntimeException("Class {$className} not found");
530
443
  }
531
444
 
445
+ self::cacheClassReflection($className);
446
+
532
447
  if (!isset(self::$reflections[$className])) {
533
448
  $rc = new ReflectionClass($className);
534
449
  self::$reflections[$className] = $rc;
535
450
  self::$constructors[$className] = $rc->getConstructor();
536
451
  self::$publicProperties[$className] = array_filter(
537
452
  $rc->getProperties(ReflectionProperty::IS_PUBLIC),
538
- fn(ReflectionProperty $p) => ! $p->isStatic()
453
+ fn(ReflectionProperty $p) => !$p->isStatic()
539
454
  );
540
455
  }
541
456
 
457
+ self::validateComponentProps($className, $attributes);
458
+
542
459
  $ref = self::$reflections[$className];
543
460
  $ctor = self::$constructors[$className];
544
461
  $inst = $ref->newInstanceWithoutConstructor();
@@ -560,6 +477,51 @@ class TemplateCompiler
560
477
  return $inst;
561
478
  }
562
479
 
480
+ private static function cacheClassReflection(string $className): void
481
+ {
482
+ if (isset(self::$reflections[$className])) {
483
+ return;
484
+ }
485
+
486
+ $rc = new ReflectionClass($className);
487
+ self::$reflections[$className] = $rc;
488
+ self::$constructors[$className] = $rc->getConstructor();
489
+
490
+ $publicProps = array_filter(
491
+ $rc->getProperties(ReflectionProperty::IS_PUBLIC),
492
+ fn(ReflectionProperty $p) => !$p->isStatic()
493
+ );
494
+ self::$publicProperties[$className] = $publicProps;
495
+
496
+ $allowed = self::SYSTEM_PROPS;
497
+ foreach ($publicProps as $prop) {
498
+ $allowed[$prop->getName()] = true;
499
+ }
500
+ self::$allowedProps[$className] = $allowed;
501
+ }
502
+
503
+ private static function validateComponentProps(string $className, array $attributes): void
504
+ {
505
+ foreach (self::$publicProperties[$className] as $prop) {
506
+ $name = $prop->getName();
507
+ $type = $prop->getType();
508
+
509
+ if (
510
+ $type instanceof ReflectionNamedType && $type->isBuiltin()
511
+ && ! $type->allowsNull()
512
+ && ! array_key_exists($name, $attributes)
513
+ ) {
514
+ throw new ComponentValidationException(
515
+ $name,
516
+ $className,
517
+ array_map(fn($p) => $p->getName(), self::$publicProperties[$className])
518
+ );
519
+ }
520
+ }
521
+
522
+ return;
523
+ }
524
+
563
525
  private static function coerce(mixed $value, ?ReflectionType $type): mixed
564
526
  {
565
527
  if (!$type instanceof ReflectionNamedType || $type->isBuiltin() === false) {