feat(relay): render dd() responses properly (#29)
* feat(relay): render `dd()` responses correctly * wiki(relay): `dd` responses * test: fix failing test after changing test id * style: apply TS style fixes * style: apply php style fixes * chore: fix types * build: always run php and js test on PR changes * test(relay): simplify test * build: use proper cache key for e2e job * feat(relay): more assertive dd parser * build: fix package.json indentation * build: properly build for `dev` mode * test: correctly install current branch artifacts for E2E * test: fail fast for PW tests * build: don't run php and js tests twice on PRs * test: PW fixes * test: correctly build artifacts from branch for E2E * chore: fix naming * fix(relay): properly parse some type of closures in `dd` * test: enable parallel runs for PW * test(relay): simplify `dd` E2E test
This commit is contained in:
@@ -21,6 +21,7 @@ class GenerateIntellisenseCommand extends SymfonyCommand
|
||||
protected array $intellisenseProviders = [
|
||||
IntellisenseProviders\AuthorizationTypeIntellisense::class,
|
||||
IntellisenseProviders\RandomValueGeneratorIntellisense::class,
|
||||
IntellisenseProviders\DumpValueTypeIntellisense::class,
|
||||
];
|
||||
|
||||
const TARGET_BASE_PATH = '/resources/js/interfaces/generated/';
|
||||
|
||||
41
src/IntellisenseProviders/DumpValueTypeIntellisense.php
Normal file
41
src/IntellisenseProviders/DumpValueTypeIntellisense.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\IntellisenseProviders;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
use Sunchayn\Nimbus\IntellisenseProviders\Contracts\IntellisenseContract;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser\Enums\DumpValueTypeEnum;
|
||||
|
||||
/**
|
||||
* Generates TypeScript types for dump value types from the backend enum.
|
||||
*/
|
||||
class DumpValueTypeIntellisense implements IntellisenseContract
|
||||
{
|
||||
public const STUB = 'dump-value-types.ts.stub';
|
||||
|
||||
public function getTargetFileName(): string
|
||||
{
|
||||
return Str::remove('.stub', self::STUB);
|
||||
}
|
||||
|
||||
public function generate(): string
|
||||
{
|
||||
$enumCases = [];
|
||||
|
||||
foreach (DumpValueTypeEnum::cases() as $case) {
|
||||
$enumCases[] = sprintf(" %s = '%s',", $case->name, $case->value);
|
||||
}
|
||||
|
||||
$enumContent = implode("\n", $enumCases);
|
||||
|
||||
return $this->replaceStubContent($enumContent);
|
||||
}
|
||||
|
||||
private function replaceStubContent(string $enumList): string
|
||||
{
|
||||
$stubFile = file_get_contents(__DIR__.'/stubs/'.self::STUB) ?: throw new RuntimeException('Cannot read stub file.');
|
||||
|
||||
return str_replace('{{ content }}', rtrim($enumList), $stubFile);
|
||||
}
|
||||
}
|
||||
3
src/IntellisenseProviders/stubs/dump-value-types.ts.stub
Normal file
3
src/IntellisenseProviders/stubs/dump-value-types.ts.stub
Normal file
@@ -0,0 +1,3 @@
|
||||
export enum DumpValueType {
|
||||
{{ content }}
|
||||
}
|
||||
@@ -7,15 +7,17 @@ use GuzzleHttp\Cookie\SetCookie;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Cookie\CookieValuePrefix;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use RuntimeException;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Handlers\AuthorizationHandlerFactory;
|
||||
use Sunchayn\Nimbus\Modules\Relay\DataTransferObjects\RelayedRequestResponseData;
|
||||
use Sunchayn\Nimbus\Modules\Relay\DataTransferObjects\RequestRelayData;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Responses\DumpAndDieResponse;
|
||||
use Sunchayn\Nimbus\Modules\Relay\ValueObjects\PrintableResponseBody;
|
||||
use Sunchayn\Nimbus\Modules\Relay\ValueObjects\ResponseCookieValueObject;
|
||||
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
||||
|
||||
class RequestRelayAction
|
||||
{
|
||||
@@ -25,6 +27,7 @@ class RequestRelayAction
|
||||
|
||||
public const NON_STANDARD_STATUS_CODES = [
|
||||
419 => 'Method Not Allowed',
|
||||
DumpAndDieResponse::DUMP_AND_DIE_STATUS_CODE => 'dd()',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
@@ -44,10 +47,7 @@ class RequestRelayAction
|
||||
|
||||
$start = hrtime(true);
|
||||
|
||||
$response = $pendingRequest->send(
|
||||
method: $requestRelayData->method,
|
||||
url: $requestRelayData->endpoint,
|
||||
);
|
||||
$response = $this->sendPendingRequest($pendingRequest, $requestRelayData);
|
||||
|
||||
$durationInMs = $this->calculateDuration($start);
|
||||
|
||||
@@ -142,8 +142,26 @@ class RequestRelayAction
|
||||
*/
|
||||
private function getStatusTextFromCode(int $statusCode): string
|
||||
{
|
||||
$statusCodeToTextMapping = Response::$statusTexts + self::NON_STANDARD_STATUS_CODES;
|
||||
$statusCodeToTextMapping = SymfonyResponse::$statusTexts + self::NON_STANDARD_STATUS_CODES;
|
||||
|
||||
return $statusCodeToTextMapping[$statusCode] ?? 'Non-standard Status Code.';
|
||||
}
|
||||
|
||||
private function sendPendingRequest(PendingRequest $pendingRequest, RequestRelayData $requestRelayData): Response
|
||||
{
|
||||
$response = $pendingRequest->send(
|
||||
method: $requestRelayData->method,
|
||||
url: $requestRelayData->endpoint,
|
||||
);
|
||||
|
||||
if (! $response->serverError()) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if (! str_contains($response->body(), 'Sfdump = window.Sfdump')) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return new DumpAndDieResponse($response->toPsrResponse());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser\DataTransferObjects;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
|
||||
/**
|
||||
* @implements Arrayable<string, string|array>
|
||||
*/
|
||||
readonly class ObjectPropertyDto implements Arrayable
|
||||
{
|
||||
public function __construct(
|
||||
public string $visibility,
|
||||
public ParsedValueDto $value,
|
||||
) {}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'visibility' => $this->visibility,
|
||||
'value' => $this->value->toArray(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser\DataTransferObjects;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
|
||||
/**
|
||||
* @implements Arrayable<string, string|array>
|
||||
*/
|
||||
class ParseResultDto implements Arrayable
|
||||
{
|
||||
/**
|
||||
* @param ParsedValueDto[] $dumps
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly ?string $source,
|
||||
public readonly array $dumps,
|
||||
) {}
|
||||
|
||||
public static function empty(): self
|
||||
{
|
||||
return new self(
|
||||
source: null,
|
||||
dumps: [],
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'source' => $this->source,
|
||||
'dumps' => collect($this->dumps)->toArray(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser\DataTransferObjects;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
|
||||
/**
|
||||
* @implements Arrayable<string, int|array>
|
||||
*/
|
||||
readonly class ParsedArrayResultDto implements Arrayable
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $items
|
||||
*/
|
||||
public function __construct(
|
||||
public array $items,
|
||||
public bool $numericallyIndexed,
|
||||
) {}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'items' => collect($this->items)->toArray(),
|
||||
'length' => count($this->items),
|
||||
'numericallyIndexed' => $this->numericallyIndexed,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser\DataTransferObjects;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
|
||||
/**
|
||||
* @implements Arrayable<string, string|null>
|
||||
*/
|
||||
readonly class ParsedClosureResultDto implements Arrayable
|
||||
{
|
||||
public function __construct(
|
||||
public string $signature,
|
||||
public ?string $className,
|
||||
public ?string $thisReference,
|
||||
) {}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'signature' => $this->signature,
|
||||
'class' => $this->className,
|
||||
'this' => $this->thisReference,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser\DataTransferObjects;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
|
||||
/**
|
||||
* @implements Arrayable<string, string|int|null|array>
|
||||
*/
|
||||
readonly class ParsedObjectResultDto implements Arrayable
|
||||
{
|
||||
/**
|
||||
* @param ObjectPropertyDto[] $properties
|
||||
*/
|
||||
public function __construct(
|
||||
public ?string $className,
|
||||
public array $properties,
|
||||
) {}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'class' => $this->className,
|
||||
'properties' => collect($this->properties)->toArray(),
|
||||
'propertiesCount' => count($this->properties),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser\DataTransferObjects;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser\Enums\DumpValueTypeEnum;
|
||||
|
||||
/**
|
||||
* @implements Arrayable<string, string|float|int|bool|null|array>
|
||||
*/
|
||||
readonly class ParsedValueDto implements Arrayable
|
||||
{
|
||||
public function __construct(
|
||||
public DumpValueTypeEnum $type,
|
||||
public ParsedArrayResultDto|ParsedObjectResultDto|ParsedClosureResultDto|string|float|int|bool|null $value,
|
||||
) {}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->type->value,
|
||||
'value' => $this->value instanceof Arrayable ? $this->value->toArray() : $this->value,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser\Enums;
|
||||
|
||||
enum DumpValueTypeEnum: string
|
||||
{
|
||||
case Object = 'object';
|
||||
|
||||
case Array = 'array';
|
||||
|
||||
case String = 'string';
|
||||
|
||||
case Constant = 'constant';
|
||||
|
||||
case Uninitialized = 'uninitialized';
|
||||
|
||||
case Number = 'number';
|
||||
|
||||
case Closure = 'closure';
|
||||
|
||||
case Unknown = 'unknown';
|
||||
}
|
||||
384
src/Modules/Relay/Parsers/VarDumpParser/VarDumpParser.php
Normal file
384
src/Modules/Relay/Parsers/VarDumpParser/VarDumpParser.php
Normal file
@@ -0,0 +1,384 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser;
|
||||
|
||||
use Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser\DataTransferObjects\ObjectPropertyDto;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser\DataTransferObjects\ParsedArrayResultDto;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser\DataTransferObjects\ParsedClosureResultDto;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser\DataTransferObjects\ParsedObjectResultDto;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser\DataTransferObjects\ParsedValueDto;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser\DataTransferObjects\ParseResultDto;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser\Enums\DumpValueTypeEnum;
|
||||
|
||||
class VarDumpParser
|
||||
{
|
||||
private const PATTERN_LARAVEL_COMMENT = '/<span[^>]*style=["\'][^"\']*color:\s*#A0A0A0[^"\']*["\'][^>]*>\s*\/\/\s*(.+?)<\/span>/s';
|
||||
|
||||
/**
|
||||
* Parse raw HTML output containing one or more Symfony dumps
|
||||
*/
|
||||
public function parse(string $html): ParseResultDto
|
||||
{
|
||||
$html = $this->cleanHtml($html);
|
||||
$dumpSections = $this->extractDumpSections($html);
|
||||
|
||||
if ($dumpSections === []) {
|
||||
return ParseResultDto::empty();
|
||||
}
|
||||
|
||||
$dumps = [];
|
||||
foreach ($dumpSections as $dumpSection) {
|
||||
$cleanHtml = $this->removeCommentFromDump($dumpSection);
|
||||
$dumps[] = $this->parseValue(trim($cleanHtml));
|
||||
}
|
||||
|
||||
return new ParseResultDto(
|
||||
source: $this->extractComment($dumpSections[0]),
|
||||
dumps: $dumps,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean HTML by removing unnecessary elements
|
||||
*/
|
||||
private function cleanHtml(string $html): string
|
||||
{
|
||||
// Remove style and script tags
|
||||
$html = (string) preg_replace('/<style\b[^>]*>.*?<\/style>/is', '', $html);
|
||||
$html = (string) preg_replace('/<script\b[^>]*>.*?<\/script>/is', '', $html);
|
||||
|
||||
// Remove ellipsis elements for simpler parsing
|
||||
return (string) preg_replace('/<([a-z]+)\s+[^>]*class=(["\'])(?:[^"\'>]*\s)?sf-dump-ellipsis[^"\'>]*\2[^>]*>.*?<\/\1>/si', '', $html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract individual dump sections from HTML
|
||||
*
|
||||
* @return array<array-key, string>
|
||||
*/
|
||||
private function extractDumpSections(string $html): array
|
||||
{
|
||||
preg_match_all('/<pre\b[^>]*>\K.*?(?=<\/pre>)/s', $html, $matches);
|
||||
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Laravel file/line comment from dump output
|
||||
*/
|
||||
private function extractComment(string $html): ?string
|
||||
{
|
||||
if (preg_match(self::PATTERN_LARAVEL_COMMENT, $html, $match)) {
|
||||
return trim($match[1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove Laravel comment from dump HTML
|
||||
*/
|
||||
private function removeCommentFromDump(string $html): string
|
||||
{
|
||||
return (string) preg_replace(self::PATTERN_LARAVEL_COMMENT, '', $html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the root type of a dump value
|
||||
*/
|
||||
private function detectRootType(string $html): DumpValueTypeEnum
|
||||
{
|
||||
if ($html === '""') {
|
||||
return DumpValueTypeEnum::String;
|
||||
}
|
||||
|
||||
if ($html === '[]') {
|
||||
return DumpValueTypeEnum::Array;
|
||||
}
|
||||
|
||||
// Array with size notation: array:3 [
|
||||
if (preg_match('/^<span\b[^>]*class="?sf-dump-note[^>]*>\s*array:\d+/s', $html)) {
|
||||
return DumpValueTypeEnum::Array;
|
||||
}
|
||||
|
||||
// Closure (must check before object to avoid confusion with named objects)
|
||||
if (preg_match('/^<span class="?sf-dump-note[^>]*>Closure\([^)]*\)<\/span>/s', $html)) {
|
||||
return DumpValueTypeEnum::Closure;
|
||||
}
|
||||
|
||||
// Named Objects (e.g. : ClassName {), or Closures.
|
||||
if (preg_match('/^<span class="?sf-dump-note[^>]*>[^<]*<\/span>\s*\{/s', $html, $matches)) {
|
||||
// If there are parenthesis in the matched portion (the beginning of the html) then it is a Closure.
|
||||
// e.g. <span class="sf-dump-note sf-dump-ellipsization" title="Illuminate\Foundation\Application::environment(...$environments)"></span> {
|
||||
// e.g. <span class=sf-dump-note>Illuminate\Foundation\Application::environment(...$environments)</span> {
|
||||
return preg_match('/^.+\([^)]*\)/s', $matches[0])
|
||||
? DumpValueTypeEnum::Closure
|
||||
: DumpValueTypeEnum::Object;
|
||||
}
|
||||
|
||||
// Runtime object: {<a...>
|
||||
if (preg_match('/^\{<a class=sf-dump-ref/s', $html)) {
|
||||
return DumpValueTypeEnum::Object;
|
||||
}
|
||||
|
||||
// String value
|
||||
if (preg_match('/^"<span\b[^>]*class=sf-dump-str\b/s', $html)) {
|
||||
return DumpValueTypeEnum::String;
|
||||
}
|
||||
|
||||
// Uninitialized property (must be before const check).
|
||||
if (preg_match('/^<span [^>]* title="Uninitialized property">/s', $html)) {
|
||||
return DumpValueTypeEnum::Uninitialized;
|
||||
}
|
||||
|
||||
// Boolean or null constants
|
||||
if (preg_match('/^<span\b[^>]*class=sf-dump-const\b/s', $html)) {
|
||||
return DumpValueTypeEnum::Constant;
|
||||
}
|
||||
|
||||
// Numeric value
|
||||
if (preg_match('/^<span\b[^>]*class=sf-dump-num\b/s', $html)) {
|
||||
return DumpValueTypeEnum::Number;
|
||||
}
|
||||
|
||||
return DumpValueTypeEnum::Unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a value based on its detected type
|
||||
*/
|
||||
private function parseValue(string $html): ParsedValueDto
|
||||
{
|
||||
$dumpValueTypeEnum = $this->detectRootType($html);
|
||||
|
||||
$value = match ($dumpValueTypeEnum) {
|
||||
DumpValueTypeEnum::Object => $this->parseObject($html),
|
||||
DumpValueTypeEnum::Array => $this->parseArray($html),
|
||||
DumpValueTypeEnum::String => $this->parseString($html),
|
||||
DumpValueTypeEnum::Constant => $this->parseConst($html),
|
||||
DumpValueTypeEnum::Number => $this->parseNumber($html),
|
||||
DumpValueTypeEnum::Closure => $this->parseClosure($html),
|
||||
DumpValueTypeEnum::Uninitialized => $this->parseUninitialized($html),
|
||||
DumpValueTypeEnum::Unknown => null,
|
||||
};
|
||||
|
||||
return new ParsedValueDto(type: $dumpValueTypeEnum, value: $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse object structure and properties
|
||||
*/
|
||||
private function parseObject(string $html): ParsedObjectResultDto
|
||||
{
|
||||
$className = $this->extractObjectClassName($html);
|
||||
|
||||
// Extract content between { and }
|
||||
if (! preg_match('/\{<a class=sf-dump-ref[^>]*>[^<]+<\/a><samp[^>]+>(.+)<\/samp>}\n?$/s', $html, $match)) {
|
||||
// Sometimes items are not listed when a certain depth is reached.
|
||||
// e.g. Symfony\Component\Routing\CompiledRoute {#369 …8}
|
||||
return new ParsedObjectResultDto(className: $className, properties: []);
|
||||
}
|
||||
|
||||
$content = trim($match[1], "\n");
|
||||
|
||||
// Normalize indentation to simplify parsing
|
||||
$content = $this->normalizeIndentation($content);
|
||||
|
||||
$properties = [];
|
||||
|
||||
// Match properties with visibility markers: +/-/# "propertyName": value
|
||||
$pattern = '/[#\+\-]"?<span class=sf-dump-(public|protected|private)[^>]*>([^<]+)<\/span>"?:\s(.*?)(?=(?:\n[#\+\-](?:<|"<)|\Z))/s';
|
||||
|
||||
preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);
|
||||
|
||||
foreach ($matches as $match) {
|
||||
$visibility = $match[1];
|
||||
$propertyName = trim($match[2]);
|
||||
$propertyValueHtml = trim($match[3]);
|
||||
|
||||
$properties[$propertyName] = new ObjectPropertyDto(
|
||||
visibility: $visibility,
|
||||
value: $this->parseValue($propertyValueHtml),
|
||||
);
|
||||
}
|
||||
|
||||
return new ParsedObjectResultDto(className: $className, properties: $properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse array structure and items
|
||||
*/
|
||||
private function parseArray(string $html): ParsedArrayResultDto
|
||||
{
|
||||
if ($html === '[]') {
|
||||
return new ParsedArrayResultDto(items: [], numericallyIndexed: true);
|
||||
}
|
||||
|
||||
// Extract content within <samp> tags
|
||||
if (! preg_match('/^<span\b[^>]*class=sf-dump-note[^>]*>\s*array:\d+\s*<\/span>\s*\[\s*<samp\b[^>]*>(.*?)<\/samp>]$/s', $html, $match)) {
|
||||
// Sometimes items are not listed when a certain depth is reached.
|
||||
// e.g. +methods: array:2 [ …2]
|
||||
return new ParsedArrayResultDto(items: [], numericallyIndexed: true);
|
||||
}
|
||||
|
||||
$content = trim($match[1], "\n");
|
||||
|
||||
// Normalize indentation
|
||||
$content = $this->normalizeIndentation($content);
|
||||
|
||||
$items = [];
|
||||
$isNumerical = true;
|
||||
|
||||
// Match key => value pairs (works for both indexed and associative)
|
||||
$pattern = '/"?<span class=sf-dump-(key|index)[^>]*>([^<]+)<\/span>"?\s=>\s(.*?)(?=(?:\n"?<span class=sf-dump-(?:key|index))|\Z)/s';
|
||||
|
||||
preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);
|
||||
|
||||
foreach ($matches as $match) {
|
||||
$keyType = $match[1]; // 'key' or 'index'
|
||||
$key = $match[2];
|
||||
$valueHtml = trim($match[3]);
|
||||
|
||||
// If any key is not an index, the array is not numerically indexed
|
||||
if ($keyType === 'key') {
|
||||
$isNumerical = false;
|
||||
}
|
||||
|
||||
$items[$key] = $this->parseValue($valueHtml);
|
||||
}
|
||||
|
||||
return new ParsedArrayResultDto(items: $items, numericallyIndexed: $isNumerical);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse string primitive value
|
||||
*/
|
||||
private function parseString(string $html): string
|
||||
{
|
||||
if ($html === '""') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (preg_match('/<span class=sf-dump-str[^>]*>([^<]*)<\/span>/', $html, $match)) {
|
||||
return html_entity_decode($match[1], ENT_QUOTES | ENT_HTML5);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse constant value (true, false, null)
|
||||
*/
|
||||
private function parseConst(string $html): ?bool
|
||||
{
|
||||
if (preg_match('/<span class=sf-dump-const[^>]*>([^<]+)<\/span>/', $html, $match)) {
|
||||
return match (strtolower(trim($match[1]))) {
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse numeric value (int or float)
|
||||
*/
|
||||
private function parseNumber(string $html): int|float
|
||||
{
|
||||
if (preg_match('/<span class=sf-dump-num[^>]*>([^<]+)<\/span>/', $html, $match)) {
|
||||
$value = $match[1];
|
||||
|
||||
return str_contains($value, '.') ? (float) $value : (int) $value;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse closure information
|
||||
*/
|
||||
private function parseClosure(string $html): ParsedClosureResultDto
|
||||
{
|
||||
$signature = 'CLosure()';
|
||||
$className = null;
|
||||
$thisReference = null;
|
||||
|
||||
// Extract signature from between span tags.
|
||||
if (preg_match('/^<span[^>]*>([^<]+)/', $html, $match)) {
|
||||
$signature = trim($match[1]);
|
||||
}
|
||||
// Extract signature from the title
|
||||
elseif (preg_match('/^<span[^>]*title="([^"\n]+)/', $html, $match)) {
|
||||
$signature = trim($match[1]);
|
||||
}
|
||||
|
||||
// Extract class context
|
||||
if (preg_match('/<span [^>]*class=sf-dump-meta\s*>class<\/span>:\s*"?<span [^>]*title="([^"\n]+)/', $html, $match)) {
|
||||
$className = trim($match[1]);
|
||||
}
|
||||
|
||||
// Extract this reference
|
||||
if (preg_match('/<span [^>]*class=sf-dump-meta\s*>this<\/span>:\s*"?<span [^>]*title="([^"\n]+)/', $html, $match)) {
|
||||
$thisReference = trim($match[1]);
|
||||
}
|
||||
|
||||
return new ParsedClosureResultDto(
|
||||
signature: $signature,
|
||||
className: $className,
|
||||
thisReference: $thisReference,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse uninitialized property annotation
|
||||
*/
|
||||
private function parseUninitialized(string $html): string
|
||||
{
|
||||
if (preg_match('/^<span[^>]+>([^<]+)<\/span>$/', $html, $match)) {
|
||||
return $match[1];
|
||||
}
|
||||
|
||||
return 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract class name from object dump
|
||||
*/
|
||||
private function extractObjectClassName(string $html): ?string
|
||||
{
|
||||
// Check for title attribute (ellipsized class names)
|
||||
if (preg_match('/^<span[^>]*title="([^"\n\s]+)/', $html, $match)) {
|
||||
return trim($match[1]);
|
||||
}
|
||||
|
||||
// Check for class name in sf-dump-note span
|
||||
if (preg_match('/^<span class="?sf-dump-note[^>]*>([^<]+)<\/span>/s', $html, $match)) {
|
||||
return html_entity_decode(strip_tags(trim($match[1])), ENT_QUOTES | ENT_HTML5);
|
||||
}
|
||||
|
||||
// Runtime object (no explicit class name)
|
||||
if (preg_match('/<a class=sf-dump-ref[^>]*>/', $html)) {
|
||||
return '<runtime object>';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize indentation by removing common leading whitespace
|
||||
*/
|
||||
private function normalizeIndentation(string $content): string
|
||||
{
|
||||
// Find the indentation of the first line
|
||||
if (preg_match('/^(\s+)/s', $content, $match)) {
|
||||
$indent = $match[1];
|
||||
|
||||
// Remove this indentation from all lines
|
||||
return (string) preg_replace("/\n{$indent}/", "\n", ltrim($content));
|
||||
}
|
||||
|
||||
return ltrim($content);
|
||||
}
|
||||
}
|
||||
35
src/Modules/Relay/Responses/DumpAndDieResponse.php
Normal file
35
src/Modules/Relay/Responses/DumpAndDieResponse.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Relay\Responses;
|
||||
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Str;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Parsers\VarDumpParser\VarDumpParser;
|
||||
|
||||
class DumpAndDieResponse extends Response
|
||||
{
|
||||
public const DUMP_AND_DIE_STATUS_CODE = 999;
|
||||
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return self::DUMP_AND_DIE_STATUS_CODE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|array<array-key, mixed>>
|
||||
*/
|
||||
public function json($key = null, $default = null): array
|
||||
{
|
||||
[
|
||||
'source' => $source,
|
||||
'dumps' => $dumps,
|
||||
] = resolve(VarDumpParser::class)->parse($this->response->getBody())->toArray();
|
||||
|
||||
return [
|
||||
'id' => Str::uuid()->toString(),
|
||||
'timestamp' => now()->format('Y-m-d H:i:s'),
|
||||
'source' => $source,
|
||||
'dumps' => $dumps,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user