create-prisma-php-app 4.0.0-alpha.19 → 4.0.0-alpha.20

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.
@@ -4,203 +4,187 @@ declare(strict_types=1);
4
4
 
5
5
  namespace Lib\Headers;
6
6
 
7
+ use InvalidArgumentException;
8
+ use JsonException;
9
+
10
+ /**
11
+ * HTTP‑error helper.
12
+ *
13
+ * @method static self badRequest(string $message = 'Bad Request', array $details = [])
14
+ * @method static self unauthorized(string $message = 'Unauthorized', array $details = [])
15
+ * @method static self paymentRequired(string $message = 'Payment Required', array $details = [])
16
+ * @method static self forbidden(string $message = 'Forbidden', array $details = [])
17
+ * @method static self notFound(string $message = 'Not Found', array $details = [])
18
+ * @method static self methodNotAllowed(string $message = 'Method Not Allowed', array $details = [])
19
+ * @method static self notAcceptable(string $message = 'Not Acceptable', array $details = [])
20
+ * @method static self conflict(string $message = 'Conflict', array $details = [])
21
+ * @method static self gone(string $message = 'Gone', array $details = [])
22
+ * @method static self lengthRequired(string $message = 'Length Required', array $details = [])
23
+ * @method static self preconditionFailed(string $message = 'Precondition Failed', array $details = [])
24
+ * @method static self payloadTooLarge(string $message = 'Payload Too Large', array $details = [])
25
+ * @method static self uriTooLarge(string $message = 'URI Too Large', array $details = [])
26
+ * @method static self unsupportedMediaType(string $message = 'Unsupported Media Type', array $details = [])
27
+ * @method static self rangeNotSatisfiable(string $message = 'Range Not Satisfiable', array $details = [])
28
+ * @method static self expectationFailed(string $message = 'Expectation Failed', array $details = [])
29
+ * @method static self iAmATeapot(string $message = "I'm a teapot", array $details = [])
30
+ * @method static self misdirectedRequest(string $message = 'Misdirected Request', array $details = [])
31
+ * @method static self unprocessableEntity(string $message = 'Unprocessable Entity', array $details = [])
32
+ * @method static self locked(string $message = 'Locked', array $details = [])
33
+ * @method static self failedDependency(string $message = 'Failed Dependency', array $details = [])
34
+ * @method static self tooEarly(string $message = 'Too Early', array $details = [])
35
+ * @method static self upgradeRequired(string $message = 'Upgrade Required', array $details = [])
36
+ * @method static self preconditionRequired(string $message = 'Precondition Required', array $details = [])
37
+ * @method static self tooManyRequests(string $message = 'Too Many Requests', array $details = [])
38
+ * @method static self requestHeaderFieldsTooLarge(string $message = 'Request Header Fields Too Large', array $details = [])
39
+ * @method static self unavailableForLegalReasons(string $message = 'Unavailable for Legal Reasons', array $details = [])
40
+ * @method static self internal(string $message = 'Internal Server Error', array $details = [])
41
+ * @method static self notImplemented(string $message = 'Not Implemented', array $details = [])
42
+ * @method static self badGateway(string $message = 'Bad Gateway', array $details = [])
43
+ * @method static self serviceUnavailable(string $message = 'Service Unavailable', array $details = [])
44
+ * @method static self gatewayTimeout(string $message = 'Gateway Timeout', array $details = [])
45
+ * @method static self httpVersionNotSupported(string $message = 'HTTP Version Not Supported', array $details = [])
46
+ * @method static self insufficientStorage(string $message = 'Insufficient Storage', array $details = [])
47
+ * @method static self loopDetected(string $message = 'Loop Detected', array $details = [])
48
+ * @method static self notExtended(string $message = 'Not Extended', array $details = [])
49
+ * @method static self networkAuthenticationRequired(string $message = 'Network Authentication Required', array $details = [])
50
+ * @method static self networkConnectTimeoutError(string $message = 'Network Connect Timeout Error', array $details = [])
51
+ */
7
52
  class Boom
8
53
  {
9
- /**
10
- * HTTP status code.
11
- *
12
- * @var int
13
- */
14
- protected int $statusCode = 500;
15
-
16
- /**
17
- * Error message.
18
- *
19
- * @var string
20
- */
21
- protected string $errorMessage = 'Internal Server Error';
22
-
23
- /**
24
- * Additional error details.
25
- *
26
- * @var array
27
- */
28
- protected array $errorDetails = [];
29
-
30
- /**
31
- * Boom constructor.
32
- *
33
- * @param int $statusCode HTTP status code.
34
- * @param string $errorMessage Error message.
35
- * @param array $errorDetails Additional error details.
36
- */
37
- public function __construct(int $statusCode, string $errorMessage, array $errorDetails = [])
38
- {
39
- $this->statusCode = $statusCode;
40
- $this->errorMessage = $errorMessage;
41
- $this->errorDetails = $errorDetails;
42
- }
43
-
44
- /**
45
- * Factory method for 400 Bad Request.
46
- *
47
- * @param string $message Error message.
48
- * @param array $details Additional error details.
49
- *
50
- * @return self
51
- */
52
- public static function badRequest(string $message = 'Bad Request', array $details = []): self
53
- {
54
- return new self(400, $message, $details);
54
+ private const PHRASES = [
55
+ /* 4XX Client error */
56
+ 400 => 'Bad Request',
57
+ 401 => 'Unauthorized',
58
+ 402 => 'Payment Required',
59
+ 403 => 'Forbidden',
60
+ 404 => 'Not Found',
61
+ 405 => 'Method Not Allowed',
62
+ 406 => 'Not Acceptable',
63
+ 407 => 'Proxy Authentication Required',
64
+ 408 => 'Request Timeout',
65
+ 409 => 'Conflict',
66
+ 410 => 'Gone',
67
+ 411 => 'Length Required',
68
+ 412 => 'Precondition Failed',
69
+ 413 => 'Payload Too Large',
70
+ 414 => 'URI Too Large',
71
+ 415 => 'Unsupported Media Type',
72
+ 416 => 'Range Not Satisfiable',
73
+ 417 => 'Expectation Failed',
74
+ 418 => "I'm a teapot",
75
+ 421 => 'Misdirected Request',
76
+ 422 => 'Unprocessable Entity',
77
+ 423 => 'Locked',
78
+ 424 => 'Failed Dependency',
79
+ 425 => 'Too Early',
80
+ 426 => 'Upgrade Required',
81
+ 428 => 'Precondition Required',
82
+ 429 => 'Too Many Requests',
83
+ 431 => 'Request Header Fields Too Large',
84
+ 451 => 'Unavailable for Legal Reasons',
85
+ 499 => 'Client Closed Request',
86
+
87
+ /* 5XX Server error */
88
+ 500 => 'Internal Server Error',
89
+ 501 => 'Not Implemented',
90
+ 502 => 'Bad Gateway',
91
+ 503 => 'Service Unavailable',
92
+ 504 => 'Gateway Timeout',
93
+ 505 => 'HTTP Version Not Supported',
94
+ 507 => 'Insufficient Storage',
95
+ 508 => 'Loop Detected',
96
+ 510 => 'Not Extended',
97
+ 511 => 'Network Authentication Required',
98
+ 599 => 'Network Connect Timeout Error',
99
+ ];
100
+
101
+ /** @var int */
102
+ protected int $statusCode;
103
+
104
+ /** @var string */
105
+ protected string $errorMessage;
106
+
107
+ /** @var array<string,mixed> */
108
+ protected array $errorDetails;
109
+
110
+ public function __construct(
111
+ int $statusCode,
112
+ string $errorMessage = '',
113
+ array $errorDetails = [],
114
+ ) {
115
+ if (!isset(self::PHRASES[$statusCode])) {
116
+ throw new InvalidArgumentException("Unsupported HTTP status code: $statusCode");
117
+ }
118
+
119
+ $this->statusCode = $statusCode;
120
+ $this->errorMessage = $errorMessage ?: self::PHRASES[$statusCode];
121
+ $this->errorDetails = $errorDetails;
55
122
  }
56
123
 
57
- /**
58
- * Factory method for 401 Unauthorized.
59
- *
60
- * @param string $message Error message.
61
- * @param array $details Additional error details.
62
- *
63
- * @return self
64
- */
65
- public static function unauthorized(string $message = 'Unauthorized', array $details = []): self
124
+ public static function create(int $code, ?string $msg = null, array $details = []): self
66
125
  {
67
- return new self(401, $message, $details);
126
+ return new self($code, $msg ?? '', $details);
68
127
  }
69
128
 
70
129
  /**
71
- * Factory method for 402 Payment Required.
72
- *
73
- * @param string $message Error message.
74
- * @param array $details Additional error details.
75
- *
76
- * @return self
77
- */
78
- public static function paymentRequired(string $message = 'Payment Required', array $details = []): self
79
- {
80
- return new self(402, $message, $details);
81
- }
82
-
83
- /**
84
- * Factory method for 403 Forbidden.
85
- *
86
- * @param string $message Error message.
87
- * @param array $details Additional error details.
88
- *
89
- * @return self
90
- */
91
- public static function forbidden(string $message = 'Forbidden', array $details = []): self
92
- {
93
- return new self(403, $message, $details);
94
- }
95
-
96
- /**
97
- * Factory method for 404 Not Found.
98
- *
99
- * @param string $message Error message.
100
- * @param array $details Additional error details.
130
+ * Dynamic factories: Boom::tooManyRequests(), Boom::badRequest(),
101
131
  *
102
- * @return self
132
+ * @param array{0?:string,1?:array<mixed>} $args
103
133
  */
104
- public static function notFound(string $message = 'Not Found', array $details = []): self
134
+ public static function __callStatic(string $method, array $args): self
105
135
  {
106
- return new self(404, $message, $details);
136
+ // Convert camelCase to Studly Caps → Reason‑Phrase → code
137
+ $normalized = strtolower(preg_replace('/([a-z])([A-Z])/', '$1 $2', $method) ?? '');
138
+ $code = array_search(
139
+ ucwords(str_replace(' ', ' ', $normalized)),
140
+ self::PHRASES,
141
+ true
142
+ );
143
+
144
+ if ($code === false) {
145
+ throw new InvalidArgumentException("Undefined Boom factory: $method()");
146
+ }
147
+
148
+ $msg = $args[0] ?? '';
149
+ $details = $args[1] ?? [];
150
+
151
+ return new self((int)$code, $msg, $details);
107
152
  }
108
153
 
109
- /**
110
- * Factory method for 405 Method Not Allowed.
111
- *
112
- * @param string $message Error message.
113
- * @param array $details Additional error details.
114
- *
115
- * @return self
116
- */
117
- public static function methodNotAllowed(string $message = 'Method Not Allowed', array $details = []): self
118
- {
119
- return new self(405, $message, $details);
120
- }
121
-
122
- /**
123
- * Factory method for 406 Not Acceptable.
124
- *
125
- * @param string $message Error message.
126
- * @param array $details Additional error details.
127
- *
128
- * @return self
129
- */
130
- public static function notAcceptable(string $message = 'Not Acceptable', array $details = []): self
131
- {
132
- return new self(406, $message, $details);
133
- }
134
-
135
- /**
136
- * Factory method for 500 Internal Server Error.
137
- *
138
- * @param string $message Error message.
139
- * @param array $details Additional error details.
140
- *
141
- * @return self
142
- */
143
- public static function internal(string $message = 'Internal Server Error', array $details = []): self
144
- {
145
- return new self(500, $message, $details);
146
- }
147
-
148
- /**
149
- * Sends the HTTP error response and terminates the script.
150
- *
151
- * @return void
152
- */
153
154
  public function toResponse(): void
154
155
  {
155
156
  http_response_code($this->statusCode);
156
- header('Content-Type: application/json');
157
-
158
- echo json_encode([
159
- 'statusCode' => $this->statusCode,
160
- 'error' => $this->errorMessage,
161
- 'details' => $this->errorDetails,
162
- ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);
163
-
164
- exit; // Ensures no further execution after sending the response
157
+ header('Content-Type: application/json; charset=utf-8');
158
+
159
+ try {
160
+ echo json_encode(
161
+ [
162
+ 'statusCode' => $this->statusCode,
163
+ 'error' => $this->errorMessage,
164
+ 'details' => (object)$this->errorDetails,
165
+ ],
166
+ JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
167
+ );
168
+ } catch (JsonException $e) {
169
+ echo '{"statusCode":500,"error":"JSON encoding error"}';
170
+ }
171
+
172
+ exit; // Ensure no further output
165
173
  }
166
174
 
167
- /**
168
- * Checks if the provided error is an instance of Boom.
169
- *
170
- * @param mixed $error The error to check.
171
- *
172
- * @return bool
173
- */
174
- public static function isBoom($error): bool
175
+ public static function isBoom(mixed $err): bool
175
176
  {
176
- return $error instanceof self;
177
+ return $err instanceof self;
177
178
  }
178
179
 
179
- /**
180
- * Gets the HTTP status code.
181
- *
182
- * @return int
183
- */
184
180
  public function getStatusCode(): int
185
181
  {
186
182
  return $this->statusCode;
187
183
  }
188
-
189
- /**
190
- * Gets the error message.
191
- *
192
- * @return string
193
- */
194
184
  public function getErrorMessage(): string
195
185
  {
196
186
  return $this->errorMessage;
197
187
  }
198
-
199
- /**
200
- * Gets the additional error details.
201
- *
202
- * @return array
203
- */
204
188
  public function getErrorDetails(): array
205
189
  {
206
190
  return $this->errorDetails;
@@ -10,7 +10,7 @@ use Lib\PHPX\TemplateCompiler;
10
10
 
11
11
  class IncludeTracker
12
12
  {
13
- public static array $sections = [];
13
+ private static array $sections = [];
14
14
 
15
15
  /**
16
16
  * Includes and echoes a file wrapped in a unique pp-component container.
@@ -0,0 +1,104 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Lib\MCP;
6
+
7
+ use GuzzleHttp\Client;
8
+ use GuzzleHttp\Exception\GuzzleException;
9
+ use PhpMcp\Server\Attributes\McpTool;
10
+ use PhpMcp\Server\Attributes\Schema;
11
+ use RuntimeException;
12
+ use JsonException;
13
+
14
+ final class WeatherTools
15
+ {
16
+ #[McpTool(
17
+ name: 'get-weathers',
18
+ description: 'Returns current temperature for a city'
19
+ )]
20
+ public function getWeather(
21
+ #[Schema(type: 'string', minLength: 1, description: 'City name')]
22
+ string $city
23
+ ): string {
24
+ $http = new Client([
25
+ 'timeout' => 10,
26
+ 'connect_timeout' => 8,
27
+ 'http_errors' => true,
28
+ 'headers' => ['Accept' => 'application/json'],
29
+ ]);
30
+
31
+ try {
32
+ $geoUrl = 'https://geocoding-api.open-meteo.com/v1/search'
33
+ . '?name=' . rawurlencode($city)
34
+ . '&count=1&language=en&format=json';
35
+
36
+ $geoRes = $http->get($geoUrl);
37
+ $geoJson = $geoRes->getBody()->getContents();
38
+ $geo = json_decode($geoJson, true, 512, JSON_THROW_ON_ERROR);
39
+
40
+ if (empty($geo['results'][0])) {
41
+ throw new RuntimeException("City \"$city\" not found (geocoding returned no results).");
42
+ }
43
+
44
+ $lat = (float) $geo['results'][0]['latitude'];
45
+ $lon = (float) $geo['results'][0]['longitude'];
46
+
47
+ $wxUrl = "https://api.open-meteo.com/v1/forecast?latitude={$lat}&longitude={$lon}&current_weather=true";
48
+
49
+ $wxRes = $http->get($wxUrl);
50
+ $wxJson = $wxRes->getBody()->getContents();
51
+ $wx = json_decode($wxJson, true, 512, JSON_THROW_ON_ERROR);
52
+
53
+ $cw = $wx['current_weather'] ?? null;
54
+ if (!is_array($cw)) {
55
+ throw new RuntimeException('No current weather data found in API response.');
56
+ }
57
+
58
+ $map = [
59
+ 0 => 'Clear sky',
60
+ 1 => 'Mainly clear',
61
+ 2 => 'Partly cloudy',
62
+ 3 => 'Overcast',
63
+ 45 => 'Fog',
64
+ 48 => 'Depositing rime fog',
65
+ 51 => 'Light drizzle',
66
+ 53 => 'Moderate drizzle',
67
+ 55 => 'Dense drizzle',
68
+ 56 => 'Light freezing drizzle',
69
+ 57 => 'Dense freezing drizzle',
70
+ 61 => 'Slight rain',
71
+ 63 => 'Moderate rain',
72
+ 65 => 'Heavy rain',
73
+ 66 => 'Light freezing rain',
74
+ 67 => 'Heavy freezing rain',
75
+ 71 => 'Slight snow fall',
76
+ 73 => 'Moderate snow fall',
77
+ 75 => 'Heavy snow fall',
78
+ 77 => 'Snow grains',
79
+ 80 => 'Slight rain showers',
80
+ 81 => 'Moderate rain showers',
81
+ 82 => 'Violent rain showers',
82
+ 85 => 'Slight snow showers',
83
+ 86 => 'Heavy snow showers',
84
+ 95 => 'Thunderstorm',
85
+ 96 => 'Thunderstorm with slight hail',
86
+ 99 => 'Thunderstorm with heavy hail',
87
+ ];
88
+
89
+ $code = (int)($cw['weathercode'] ?? -1);
90
+ $tempC = $cw['temperature'] ?? null;
91
+
92
+ if (!is_numeric($tempC)) {
93
+ throw new RuntimeException('Temperature missing or not numeric in current weather payload.');
94
+ }
95
+
96
+ $desc = $map[$code] ?? 'Unknown';
97
+ return "🌡️ {$tempC} °C · {$desc} (code {$code})";
98
+ } catch (GuzzleException $e) {
99
+ throw new RuntimeException('Weather request failed: ' . $e->getMessage(), previous: $e);
100
+ } catch (JsonException $e) {
101
+ throw new RuntimeException('Weather JSON parsing failed: ' . $e->getMessage(), previous: $e);
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,80 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Lib\MCP;
6
+
7
+ $root = dirname(__DIR__, 3);
8
+ require $root . '/vendor/autoload.php';
9
+ require $root . '/settings/paths.php';
10
+
11
+ use Dotenv\Dotenv;
12
+ use PhpMcp\Server\Server;
13
+ use PhpMcp\Server\Transports\StreamableHttpServerTransport;
14
+ use Throwable;
15
+
16
+ // ── Load .env (optional) and timezone ──────────────────────────────────────────
17
+ if (file_exists(DOCUMENT_PATH . '/.env')) {
18
+ Dotenv::createImmutable(DOCUMENT_PATH)->safeLoad();
19
+ }
20
+ date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'UTC');
21
+
22
+ // ── Resolve settings (with sane defaults) ─────────────────────────────────────
23
+ $appName = $_ENV['MCP_NAME'] ?? 'prisma-php-mcp';
24
+ $appVersion = $_ENV['MCP_VERSION'] ?? '0.0.1';
25
+ $host = $_ENV['MCP_HOST'] ?? '127.0.0.1';
26
+ $port = (int)($_ENV['MCP_PORT'] ?? 4000);
27
+ $prefix = trim($_ENV['MCP_PATH_PREFIX'] ?? 'mcp', '/');
28
+ $enableJson = filter_var($_ENV['MCP_JSON_RESPONSE'] ?? 'false', FILTER_VALIDATE_BOOLEAN);
29
+
30
+ // ── Build server and discover tools ───────────────────────────────────────────
31
+ $server = Server::make()
32
+ ->withServerInfo($appName, $appVersion)
33
+ ->build();
34
+
35
+ // Scan your source tree for #[McpTool] classes
36
+ $server->discover(DOCUMENT_PATH, ['src']);
37
+
38
+ // ── Pretty console output ─────────────────────────────────────────────────────
39
+ $pid = getmypid() ?: 0;
40
+ $base = "http://{$host}:{$port}/{$prefix}";
41
+ $color = static fn(string $t, string $c) => "\033[{$c}m{$t}\033[0m";
42
+
43
+ echo PHP_EOL;
44
+ echo $color("⚙ {$appName} (v{$appVersion})", '1;36') . PHP_EOL;
45
+ echo $color("→ MCP server starting…", '33') . PHP_EOL;
46
+ echo " Host: {$host}" . PHP_EOL;
47
+ echo " Port: {$port}" . PHP_EOL;
48
+ echo " Path: /{$prefix}" . PHP_EOL;
49
+ echo " JSON resp: " . ($enableJson ? 'enabled' : 'disabled') . PHP_EOL;
50
+ echo " PID: {$pid}" . PHP_EOL;
51
+ echo " URL: {$base}" . PHP_EOL;
52
+
53
+ // ── Graceful shutdown (if pcntl is available) ─────────────────────────────────
54
+ if (function_exists('pcntl_signal')) {
55
+ $stop = function (int $sig) use ($color) {
56
+ echo PHP_EOL . $color("⏹ Caught signal {$sig}. Shutting down…", '33') . PHP_EOL;
57
+ exit(0);
58
+ };
59
+ pcntl_signal(SIGINT, $stop);
60
+ pcntl_signal(SIGTERM, $stop);
61
+ }
62
+
63
+ // ── Listen ────────────────────────────────────────────────────────────────────
64
+ try {
65
+ $transport = new StreamableHttpServerTransport(
66
+ $host,
67
+ $port,
68
+ $prefix,
69
+ null, // sslContext
70
+ true, // logger
71
+ $enableJson // enableJsonResponse
72
+ // , false // (optional) stateless
73
+ );
74
+ echo $color("✓ Listening on {$base}", '32') . PHP_EOL;
75
+ $server->listen($transport);
76
+ echo PHP_EOL . $color('✔ Server stopped.', '32') . PHP_EOL;
77
+ } catch (Throwable $e) {
78
+ fwrite(STDERR, $color('✖ Server error: ', '31') . $e->getMessage() . PHP_EOL);
79
+ exit(1);
80
+ }
@@ -20,6 +20,8 @@ class MainLayout
20
20
  private static ?Set $footerScripts = null;
21
21
  private static array $customMetadata = [];
22
22
 
23
+ private static array $processedScripts = [];
24
+
23
25
  public static function init(): void
24
26
  {
25
27
  if (self::$headScripts === null) {
@@ -28,6 +30,7 @@ class MainLayout
28
30
  if (self::$footerScripts === null) {
29
31
  self::$footerScripts = new Set();
30
32
  }
33
+ self::$processedScripts = [];
31
34
  }
32
35
 
33
36
  /**
@@ -55,11 +58,19 @@ class MainLayout
55
58
  $callerClass = $trace[1]['class'] ?? 'Unknown';
56
59
 
57
60
  foreach ($scripts as $script) {
61
+ $scriptKey = md5(trim($script));
62
+
58
63
  if (strpos($script, '<script') !== false) {
59
64
  $taggedScript = "<!-- class:" . $callerClass . " -->\n" . $script;
60
- self::$footerScripts->add($taggedScript);
65
+ if (!isset(self::$processedScripts[$scriptKey])) {
66
+ self::$footerScripts->add($taggedScript);
67
+ self::$processedScripts[$scriptKey] = true;
68
+ }
61
69
  } else {
62
- self::$footerScripts->add($script);
70
+ if (!isset(self::$processedScripts[$scriptKey])) {
71
+ self::$footerScripts->add($script);
72
+ self::$processedScripts[$scriptKey] = true;
73
+ }
63
74
  }
64
75
  }
65
76
  }
@@ -97,6 +108,7 @@ class MainLayout
97
108
  public static function outputFooterScripts(): string
98
109
  {
99
110
  $processed = [];
111
+ $componentCounter = 0;
100
112
 
101
113
  foreach (self::$footerScripts->values() as $script) {
102
114
  if (preg_match('/<!-- class:([^\s]+) -->/', $script, $matches)) {
@@ -106,9 +118,11 @@ class MainLayout
106
118
  if (str_starts_with(trim($script), '<script')) {
107
119
  $script = preg_replace_callback(
108
120
  '/<script\b([^>]*)>/i',
109
- function ($m) use ($rawClassName) {
121
+ function ($m) use ($rawClassName, &$componentCounter) {
110
122
  $attrs = $m[1];
111
- $encodedClass = 's' . base_convert(sprintf('%u', crc32($rawClassName)), 10, 36);
123
+ $scriptHash = substr(md5($m[0]), 0, 8);
124
+ $encodedClass = 's' . base_convert(sprintf('%u', crc32($rawClassName . $componentCounter . $scriptHash)), 10, 36);
125
+ $componentCounter++;
112
126
 
113
127
  if (!str_contains($attrs, 'pp-component=')) {
114
128
  $attrs .= " pp-component=\"{$encodedClass}\"";
@@ -150,6 +164,7 @@ class MainLayout
150
164
  public static function clearFooterScripts(): void
151
165
  {
152
166
  self::$footerScripts->clear();
167
+ self::$processedScripts = [];
153
168
  }
154
169
 
155
170
  /**