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.
- package/dist/bootstrap.php +25 -12
- package/dist/src/Lib/ErrorHandler.php +194 -0
- package/dist/src/Lib/IncludeTracker.php +2 -41
- package/dist/src/Lib/PHPX/Exceptions/ComponentValidationException.php +49 -0
- package/dist/src/Lib/PHPX/PHPX.php +6 -11
- package/dist/src/Lib/PHPX/TemplateCompiler.php +61 -99
- package/dist/src/Lib/PrismaPHPSettings.php +0 -19
- package/dist/src/app/error.php +1 -1
- package/dist/src/app/js/index.js +1 -1
- package/package.json +1 -1
- package/dist/settings/html-events.json +0 -118
package/dist/bootstrap.php
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
1029
|
-
|
|
1030
|
-
|
|
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 =
|
|
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-
|
|
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-
|
|
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
|
-
*
|
|
49
|
-
* that the resulting CSS class string is optimized, with duplicate or conflicting
|
|
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
|
-
* -
|
|
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
|
|
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
|
|
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-
|
|
280
|
-
self::$sectionStack[] = $node->getAttribute('pp-
|
|
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 =
|
|
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-
|
|
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) =>
|
|
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) {
|