<?php
namespace Engine\Soap; trait SoapClientDebugTrait { public function debug_getIntrospection(): array { return [ 'class' => get_class($this), '__getFunctions' => $this->__getFunctions(), '__getTypes' => $this->__getTypes(), ]; } public function debug_renderIntrospectionMermaid(): string { $functions = $this->__getFunctions(); $types = $this->__getTypes(); if (!is_array($types)) { return ''; } $structs = $this->debug_parseSoapStructs($types); if (!$structs) { return ''; } ksort($structs, SORT_STRING); $serviceMethods = []; $serviceDeps = []; if (is_array($functions)) { foreach ($functions as $fn) { $parsed = $this->debug_parseSoapFunctionSignature((string)$fn); if ($parsed === null) { continue; } [$returnType, $methodName, $params] = $parsed; $paramParts = []; foreach ($params as $p) { $paramParts[] = trim($p['type'] . ' ' . $p['name']); if (isset($structs[$p['type']])) { $serviceDeps[$p['type']] = true; } } if (isset($structs[$returnType])) { $serviceDeps[$returnType] = true; } $serviceMethods[] = sprintf('+%s %s(%s)', $returnType, $methodName, implode(', ', $paramParts)); } } sort($serviceMethods, SORT_STRING); $lines = []; $lines[] = 'classDiagram'; $lines[] = 'direction LR'; if ($serviceMethods) { $lines[] = 'class SoapService {'; foreach ($serviceMethods as $m) { $lines[] = '  ' . $m; } $lines[] = '}'; } foreach ($structs as $name => $fields) { $lines[] = "class {$name} {"; foreach ($fields as $f) { $lines[] = "  +{$f['type']} {$f['name']}"; } $lines[] = '}'; } $relations = []; if ($serviceDeps) { $deps = array_keys($serviceDeps); sort($deps, SORT_STRING); foreach ($deps as $dep) { $relations["SoapService --> {$dep}"] = true; } } foreach ($structs as $name => $fields) { foreach ($fields as $f) { $target = $f['type']; if (isset($structs[$target])) { $relations["{$name} --> {$target}"] = true; } } } $relLines = array_keys($relations); sort($relLines, SORT_STRING); foreach ($relLines as $rel) { $lines[] = $rel; } return implode("\n", $lines) . "\n"; } public function debug_renderIntrospectionHtml(): string { $functions = $this->__getFunctions(); $types = $this->__getTypes(); $functionsText = ''; $typesText = ''; if (is_array($functions)) { foreach ($functions as $fn) { $line = $this->debug_unescapeSoapText((string)$fn); $functionsText .= $line . "\n"; } } else { $functionsText = var_export($functions, true); } if (is_array($types)) { foreach ($types as $type) { $block = $this->debug_unescapeSoapText((string)$type); $typesText .= rtrim($block) . "\n\n"; } $typesText = rtrim($typesText) . "\n"; } else { $typesText = var_export($types, true); } $wsdl = $this->wsdl; if ($wsdl === null || $wsdl === '') { $wsdl = '(no WSDL)'; } $title = sprintf('SOAP Introspection: %s', $wsdl); $titleEsc = htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); $classEsc = htmlspecialchars(get_class($this), ENT_QUOTES, 'UTF-8'); $functionsCount = is_array($functions) ? count($functions) : 0; $typesCount = is_array($types) ? count($types) : 0; $functionsEsc = htmlspecialchars($functionsText, ENT_QUOTES, 'UTF-8'); $typesEsc = htmlspecialchars($typesText, ENT_QUOTES, 'UTF-8'); $mermaidText = $this->debug_renderIntrospectionMermaid(); $mermaidEsc = htmlspecialchars($mermaidText, ENT_QUOTES, 'UTF-8'); $mermaidId = uniqid('tinaSoapMermaid', false); $functionsCount = (int)$functionsCount; $typesCount = (int)$typesCount; return <<<HTML
<div class="tina-soap-intro">
	<style>
		.tina-soap-intro{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;line-height:1.35;color:#111;}
		.tina-soap-intro .card{border:1px solid #d0d7de;border-radius:10px;background:#fff;box-shadow:0 1px 2px rgba(16,24,40,.06);overflow:hidden;}
		.tina-soap-intro .hdr{padding:14px 16px;border-bottom:1px solid #d0d7de;background:linear-gradient(#f6f8fa,#fff);}
		.tina-soap-intro .hdr h1{font-size:14px;margin:0 0 6px 0;font-weight:600;}
		.tina-soap-intro .meta{font-size:12px;color:#57606a;}
		.tina-soap-intro .pill{display:inline-block;font-size:12px;padding:2px 8px;border-radius:999px;background:#f6f8fa;border:1px solid #d0d7de;margin-right:6px;color:#24292f;}
		.tina-soap-intro .body{padding:12px 16px;}
		.tina-soap-intro .section{border:1px solid #d0d7de;border-radius:10px;background:#fff;margin-top:12px;overflow:hidden;}
		.tina-soap-intro .section-title{padding:10px 12px;background:#f6f8fa;border-bottom:1px solid #d0d7de;font-weight:600;font-size:13px;margin:0;}
		.tina-soap-intro .block{margin:0;padding:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:12px;white-space:pre;overflow:auto;max-height:70vh;background:#0b1020;color:#e6edf3;}
		.tina-soap-intro details.section > summary{cursor:pointer;list-style:none;}
		.tina-soap-intro details.section > summary::-webkit-details-marker{display:none;}
		.tina-soap-intro .textarea{width:100%;box-sizing:border-box;border:0;outline:none;resize:vertical;margin:0;padding:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:12px;white-space:pre;overflow:auto;max-height:70vh;min-height:200px;background:#0b1020;color:#e6edf3;}
	</style>
	<div class="card">
		<div class="hdr">
			<h1>{$titleEsc}</h1>
			<div class="meta">
				<span class="pill">functions: {$functionsCount}</span>
				<span class="pill">types: {$typesCount}</span>
			</div>
			<div class="meta">client class: <code>{$classEsc}</code></div>
		</div>
		<div class="body">
			<div class="section">
				<div class="section-title">Functions</div>
				<pre class="block">{$functionsEsc}</pre>
			</div>
			<div class="section">
				<div class="section-title">Types</div>
				<pre class="block">{$typesEsc}</pre>
			</div>
			<details class="section">
				<summary class="section-title" onclick="var el=document.getElementById('{$mermaidId}'); if(el){el.focus(); el.select();}">Mermaid (classDiagram)</summary>
				<textarea class="textarea" id="{$mermaidId}" readonly onclick="this.focus(); this.select();">{$mermaidEsc}</textarea>
			</details>
		</div>
	</div>
</div>
HTML;
} private function debug_unescapeSoapText(string $text): string { return str_replace(["\\r\\n", "\\n", "\\t"], ["\n", "\n", "\t"], $text); } private function debug_parseSoapStructs(array $types): array { $structs = []; foreach ($types as $type) { $block = trim($this->debug_unescapeSoapText((string)$type)); if ($block === '') { continue; } if (!preg_match('/^struct\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{(.*)\}\s*$/s', $block, $m)) { continue; } $name = $m[1]; $body = trim($m[2]); $fields = []; foreach (preg_split('/\n+/', $body) as $line) { $line = trim($line); if ($line === '' || $line === '}') { continue; } $line = rtrim($line, ';'); if (preg_match('/^([A-Za-z_][A-Za-z0-9_]*)\s+([A-Za-z_][A-Za-z0-9_]*)$/', $line, $fm)) { $fields[] = ['type' => $fm[1], 'name' => $fm[2]]; } } $structs[$name] = $fields; } return $structs; } private function debug_parseSoapFunctionSignature(string $signature): ?array { $sig = trim($signature); if ($sig === '') { return null; } if (!preg_match('/^(\S+)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)\s*$/', $sig, $fm)) { return null; } $returnType = $fm[1]; $methodName = $fm[2]; $paramsRaw = trim($fm[3]); $params = []; if ($paramsRaw !== '') { foreach (explode(',', $paramsRaw) as $p) { $p = trim($p); if ($p === '') { continue; } if (preg_match('/^(\S+)\s+\$?([A-Za-z_][A-Za-z0-9_]*)$/', $p, $pm)) { $params[] = ['type' => $pm[1], 'name' => $pm[2]]; } else { $params[] = ['type' => $p, 'name' => '']; } } } return [$returnType, $methodName, $params]; } public function debug_downloadwsdlAndXSD(string $targetDirectory, int $maxFiles = 500): array { $wsdlUrl = (string)$this->wsdl; $targetDirectory = rtrim($targetDirectory, '/'); $downloaded = []; $queue = []; $seen = []; $queue[] = ['url' => $wsdlUrl, 'kind' => 'wsdl']; $context = $this->debug_buildStreamContext(); $queueIndex = 0; while ($queueIndex < count($queue)) { if (count($downloaded) >= $maxFiles) { break; } $item = $queue[$queueIndex++]; $url = (string)($item['url'] ?? ''); $kind = (string)($item['kind'] ?? ''); if ($url === '') { continue; } $urlKey = strtolower($url); if (isset($seen[$urlKey])) { continue; } $seen[$urlKey] = true; $content = @file_get_contents($url, false, $context); if ($content === false) { $lastErr = error_get_last(); $downloaded[] = [ 'url' => $url, 'kind' => $kind, 'path' => null, 'error' => 'failed_to_download', 'phpError' => ($lastErr['message'] ?? null), ]; continue; } $path = $this->debug_buildDownloadPath($targetDirectory, $url, $kind); $bytes = @file_put_contents($path, $content); if ($bytes === false) { $lastErr = error_get_last(); $downloaded[] = [ 'url' => $url, 'kind' => $kind, 'path' => $path, 'error' => 'failed_to_write', 'phpError' => ($lastErr['message'] ?? null), ]; continue; } $downloaded[] = [ 'url' => $url, 'kind' => $kind, 'path' => $path, 'error' => null, 'phpError' => null, ]; $locations = $this->debug_extractSchemaLocations($content); if (!$locations) { continue; } $base = $this->debug_baseUrl($url); foreach ($locations as $loc) { $abs = $this->debug_resolveUrl($base, $loc); if ($abs === null) { continue; } $queue[] = ['url' => $abs, 'kind' => 'xsd']; } } return $downloaded; } private function debug_buildStreamContext() { $options = $this->options ? $this->options->getOptions() : []; $streamContext = []; $streamContext['ssl'] = []; if (array_key_exists('verify_peer', $options) && $options['verify_peer'] !== NULL) { if ($options['verify_peer'] === false) { $streamContext['ssl']['verify_peer'] = false; $streamContext['ssl']['verify_peer_name'] = false; $streamContext['ssl']['allow_self_signed'] = true; } } if (array_key_exists('localCertificate', $options) && $options['localCertificate'] !== NULL) { $streamContext['ssl']['local_cert'] = $options['localCertificate']['certificatePath'] ?? null; $streamContext['ssl']['passphrase'] = $options['localCertificate']['certificatePassword'] ?? null; $streamContext['ssl']['local_pk'] = $options['localCertificate']['privateKeyPath'] ?? null; } if (!empty($options['HTTPHeaders'])) { $HTTPHeaders = []; foreach ($options['HTTPHeaders'] as $HTTPHeader) { $HTTPHeaders[] = sprintf('%s: %s', $HTTPHeader['key'], str_replace("\n", '', $HTTPHeader['value'])); } $streamContext['http'] = [ 'header' => $HTTPHeaders, ]; } if (isset($options['connection_timeout']) && $options['connection_timeout'] !== NULL) { $streamContext['http']['timeout'] = (int)$options['connection_timeout']; } return stream_context_create($streamContext); } private function debug_extractSchemaLocations(string $xml): array { $locations = []; $dom = new \DOMDocument(); $prev = libxml_use_internal_errors(true); $ok = $dom->loadXML($xml); libxml_clear_errors(); libxml_use_internal_errors($prev); if (!$ok) { return []; } $xpath = new \DOMXPath($dom); $nodes = $xpath->query('//@schemaLocation'); if ($nodes) { foreach ($nodes as $n) { $val = trim((string)$n->nodeValue); if ($val !== '') { $locations[] = $val; } } } $nodes = $xpath->query('//@location'); if ($nodes) { foreach ($nodes as $n) { $val = trim((string)$n->nodeValue); if ($val !== '') { $locations[] = $val; } } } $clean = []; foreach ($locations as $loc) { $loc = trim($loc); if ($loc === '') { continue; } $clean[$loc] = true; } return array_keys($clean); } private function debug_baseUrl(string $url): string { $p = parse_url($url); if (!$p || empty($p['scheme']) || empty($p['host'])) { return $url; } $port = isset($p['port']) ? ':' . $p['port'] : ''; $path = $p['path'] ?? ''; $dir = preg_replace('#/[^/]*$#', '/', $path); return $p['scheme'] . '://' . $p['host'] . $port . $dir; } private function debug_resolveUrl(string $base, string $ref): ?string { $ref = trim($ref); if ($ref === '') { return null; } if (preg_match('#^https?://#i', $ref)) { return $ref; } if (str_starts_with($ref, '//')) { $bp = parse_url($base); if (!$bp || empty($bp['scheme'])) { return null; } return $bp['scheme'] . ':' . $ref; } if (str_starts_with($ref, '/')) { $bp = parse_url($base); if (!$bp || empty($bp['scheme']) || empty($bp['host'])) { return null; } $port = isset($bp['port']) ? ':' . $bp['port'] : ''; return $bp['scheme'] . '://' . $bp['host'] . $port . $ref; } return $base . $ref; } private function debug_buildDownloadPath(string $targetDirectory, string $url, string $kind): string { $p = parse_url($url); $host = $p['host'] ?? 'unknown-host'; $path = $p['path'] ?? ''; $base = basename($path); $query = $p['query'] ?? ''; $filename = $host.$path; if ($base) { $filename = $host.$base; } if ($query) { $filename = $host.$query; } $filename = preg_replace("/(wsdl|xsd)/", '', $filename); $filename = preg_replace('/[^A-Za-z0-9_-]+/', '_', $filename); $filename = preg_replace('/[_]+/', '_', $filename); $suffixParts = []; if ($query !== '') { $suffixParts[] = sha1($query); } $suffixParts[] = sha1($url); $path = sprintf('%s/%s %s.%s', $targetDirectory, $filename, substr(sha1(implode('', $suffixParts)), 0, 12), $kind); return $path; } } 