From a27831b3bee5aa4731f582aec482de4ba5692432 Mon Sep 17 00:00:00 2001 From: Mufthi Ryanda Date: Tue, 7 Oct 2025 13:41:28 +0700 Subject: [PATCH 01/10] feat : enhance logging --- src/Libraries/ClientExternal.php | 123 +++++++++++++++++++++++++++---- 1 file changed, 110 insertions(+), 13 deletions(-) diff --git a/src/Libraries/ClientExternal.php b/src/Libraries/ClientExternal.php index ec0bb93..fdcfa27 100644 --- a/src/Libraries/ClientExternal.php +++ b/src/Libraries/ClientExternal.php @@ -160,15 +160,23 @@ public function call(Request $request, array $options = []): ResponseInterface } $elapsed = microtime(true) - $startime; + // Parse request body $request->getBody()->rewind(); $reqbody = $request->getBody()->getContents(); - $respbody = $response->getBody()->getContents(); if (strlen($reqbody) > 5000) { - $reqbody = "more than 5000 characters"; + $parsedReqBody = "more than 5000 characters"; + } else { + $parsedReqBody = $this->parseBody($reqbody, $request->getHeader('Content-Type')); } + + // Parse response body + $respbody = $response->getBody()->getContents(); if (strlen($respbody) > 5000) { - $respbody = "more than 5000 characters"; + $parsedRespBody = "more than 5000 characters"; + } else { + $parsedRespBody = $this->parseBody($respbody, $response->getHeader('Content-Type')); } + $logData = [ 'app_name' => env('APP_NAME'), 'path' => is_null($metadata) ? null : $metadata->identifier, @@ -177,29 +185,118 @@ public function call(Request $request, array $options = []): ResponseInterface 'request' => [ 'method' => $request->getMethod(), 'headers' => $request->getHeaders(), + 'body' => $parsedReqBody ], 'response' => [ 'httpCode' => $response->getStatusCode(), 'headers' => $response->getHeaders(), + 'body' => $parsedRespBody ], 'responseTime' => round($elapsed * 1000), 'memoryUsage' => memory_get_usage() ]; - if ($request->getHeader('Content-Type') == ['application/json']) { - $logData['request']['body'] = json_decode($reqbody, true); - } else { - $logData['request']['body'] = $reqbody; - } - if ($response->getHeader('Content-Type') == ['application/json']) { - $logData['response']['body'] = json_decode($respbody, true); - } else { - $logData['response']['body'] = $respbody; - } + $response->getBody()->rewind(); Log::activity()->info($logData); return $response; } + /** + * Parse body content based on content type + * + * @param string $body Raw body content + * @param array $contentType Content-Type header + * + * @return mixed + */ + private function parseBody(string $body, array $contentType): mixed + { + if (empty($body)) { + return null; + } + + // Check length first + if (strlen($body) > 5000) { + return "[Content too large: " . strlen($body) . " characters]"; + } + + $contentTypeStr = $contentType[0] ?? ''; + + // Parse JSON + if (str_contains($contentTypeStr, 'application/json')) { + $decoded = json_decode($body, true); + return $decoded !== null ? $decoded : $body; + } + + // Parse multipart/form-data + if (str_contains($contentTypeStr, 'multipart/form-data')) { + return $this->parseMultipartFormData($body, $contentTypeStr); + } + + // Parse application/x-www-form-urlencoded + if (str_contains($contentTypeStr, 'application/x-www-form-urlencoded')) { + parse_str($body, $parsed); + return $parsed; + } + + // Return raw for other types + return $body; + } + + /** + * Parse multipart form data into readable array + * + * @param string $body Raw body content + * @param string $contentType Content-Type header value + * + * @return array + */ + private function parseMultipartFormData(string $body, string $contentType): array + { + $parsed = []; + + // Extract boundary + preg_match('/boundary=(.*)$/', $contentType, $matches); + if (empty($matches[1])) { + return ['raw' => $body]; + } + + $boundary = trim($matches[1], '"'); + $parts = explode("--$boundary", $body); + + foreach ($parts as $part) { + if (trim($part) === '' || trim($part) === '--') { + continue; + } + + // Split headers and content + $sections = preg_split('/\r?\n\r?\n/', $part, 2); + if (count($sections) < 2) { + continue; + } + + $headers = $sections[0]; + $content = trim($sections[1]); + + // Extract field name + if (preg_match('/name="([^"]+)"/', $headers, $nameMatch)) { + $name = $nameMatch[1]; + + // Check if it's a file + if (preg_match('/filename="([^"]+)"/', $headers, $fileMatch)) { + $parsed[$name] = [ + 'filename' => $fileMatch[1], + 'contents' => "[BINARY FILE DATA - " . strlen($content) . " bytes]" + ]; + } else { + $parsed[$name] = $content; + } + } + } + + return $parsed; + } + /** * Check if url shall mock * From 57c95df283c250485ebee81e0191300449661d6c Mon Sep 17 00:00:00 2001 From: Mufthi Ryanda Date: Tue, 7 Oct 2025 15:22:19 +0700 Subject: [PATCH 02/10] feat : change detect type --- src/Libraries/ClientExternal.php | 56 +++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/src/Libraries/ClientExternal.php b/src/Libraries/ClientExternal.php index fdcfa27..51d02ff 100644 --- a/src/Libraries/ClientExternal.php +++ b/src/Libraries/ClientExternal.php @@ -201,6 +201,40 @@ public function call(Request $request, array $options = []): ResponseInterface return $response; } + /** + * Detect Content Type + * + * @param string $body Raw body content + * + * @return string + */ + private function detectContentType(string $body): string + { + if (empty($body)) { + return 'empty'; + } + + // Check for JSON - try to decode + if ($body[0] === '{' || $body[0] === '[') { + json_decode($body); + if (json_last_error() === JSON_ERROR_NONE) { + return 'application/json'; + } + } + + // Check for multipart - look for boundary pattern + if (preg_match('/^--[a-zA-Z0-9\-]+/', $body)) { + return 'multipart/form-data'; + } + + // Check for URL-encoded - look for key=value pattern + if (preg_match('/^[^=]+=/', $body) && !str_contains($body, "\n")) { + return 'application/x-www-form-urlencoded'; + } + + return 'text/plain'; + } + /** * Parse body content based on content type * @@ -215,26 +249,18 @@ private function parseBody(string $body, array $contentType): mixed return null; } - // Check length first - if (strlen($body) > 5000) { - return "[Content too large: " . strlen($body) . " characters]"; - } - - $contentTypeStr = $contentType[0] ?? ''; + // Detect from body structure + $detectedType = $this->detectContentType($body); - // Parse JSON - if (str_contains($contentTypeStr, 'application/json')) { - $decoded = json_decode($body, true); - return $decoded !== null ? $decoded : $body; + if ($detectedType === 'application/json') { + return json_decode($body, true) ?? $body; } - // Parse multipart/form-data - if (str_contains($contentTypeStr, 'multipart/form-data')) { - return $this->parseMultipartFormData($body, $contentTypeStr); + if ($detectedType === 'multipart/form-data') { + return $this->parseMultipartFormData($body, 'multipart/form-data'); } - // Parse application/x-www-form-urlencoded - if (str_contains($contentTypeStr, 'application/x-www-form-urlencoded')) { + if ($detectedType === 'application/x-www-form-urlencoded') { parse_str($body, $parsed); return $parsed; } From fc04677f6dd6d0b2430b62c06660b9b6a62fe371 Mon Sep 17 00:00:00 2001 From: Mufthi Ryanda Date: Tue, 7 Oct 2025 16:03:33 +0700 Subject: [PATCH 03/10] fix : fix unused --- src/Libraries/ClientExternal.php | 63 ++++++++++++++++---------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/src/Libraries/ClientExternal.php b/src/Libraries/ClientExternal.php index 51d02ff..ddab06e 100644 --- a/src/Libraries/ClientExternal.php +++ b/src/Libraries/ClientExternal.php @@ -20,7 +20,6 @@ use Illuminate\Support\Facades\Redis; use Psr\Http\Message\ResponseInterface; use Spotlibs\PhpLib\Exceptions\InvalidRuleException; -use Spotlibs\PhpLib\Libraries\MapRoute; use Spotlibs\PhpLib\Logs\Log; use Spotlibs\PhpLib\Services\Context; use Spotlibs\PhpLib\Services\Metadata; @@ -162,19 +161,22 @@ public function call(Request $request, array $options = []): ResponseInterface // Parse request body $request->getBody()->rewind(); - $reqbody = $request->getBody()->getContents(); - if (strlen($reqbody) > 5000) { + $reqBody = $request->getBody()->getContents(); + $detectedType = $this->detectContentType($reqBody); + if ($detectedType === 'multipart/form-data') { + $parsedReqBody = $this->parseBody($reqBody); + } elseif (strlen($reqBody) > 5000) { $parsedReqBody = "more than 5000 characters"; } else { - $parsedReqBody = $this->parseBody($reqbody, $request->getHeader('Content-Type')); + $parsedReqBody = $this->parseBody($reqBody); } // Parse response body - $respbody = $response->getBody()->getContents(); - if (strlen($respbody) > 5000) { + $respBody = $response->getBody()->getContents(); + if (strlen($respBody) > 5000) { $parsedRespBody = "more than 5000 characters"; } else { - $parsedRespBody = $this->parseBody($respbody, $response->getHeader('Content-Type')); + $parsedRespBody = $this->parseBody($respBody); } $logData = [ @@ -214,42 +216,43 @@ private function detectContentType(string $body): string return 'empty'; } - // Check for JSON - try to decode - if ($body[0] === '{' || $body[0] === '[') { - json_decode($body); - if (json_last_error() === JSON_ERROR_NONE) { - return 'application/json'; - } - } - - // Check for multipart - look for boundary pattern - if (preg_match('/^--[a-zA-Z0-9\-]+/', $body)) { + // Check for multipart - look for Content-Disposition + if (preg_match('/Content-Disposition:\s*form-data/i', $body)) { + Log::runtime()->info(['detectContentType' => 'multipart']); return 'multipart/form-data'; } // Check for URL-encoded - look for key=value pattern if (preg_match('/^[^=]+=/', $body) && !str_contains($body, "\n")) { + Log::runtime()->info(['detectContentType' => 'www']); return 'application/x-www-form-urlencoded'; } + // Check for JSON - try to decode + if ($body[0] === '{' || $body[0] === '[') { + json_decode($body); + if (json_last_error() === JSON_ERROR_NONE) { + Log::runtime()->info(['detectContentType' => 'json']); + return 'application/json'; + } + } + return 'text/plain'; } /** * Parse body content based on content type * - * @param string $body Raw body content - * @param array $contentType Content-Type header + * @param string $body Raw body content * * @return mixed */ - private function parseBody(string $body, array $contentType): mixed + private function parseBody(string $body): mixed { if (empty($body)) { return null; } - // Detect from body structure $detectedType = $this->detectContentType($body); if ($detectedType === 'application/json') { @@ -257,7 +260,7 @@ private function parseBody(string $body, array $contentType): mixed } if ($detectedType === 'multipart/form-data') { - return $this->parseMultipartFormData($body, 'multipart/form-data'); + return $this->parseMultipartFormDataFromBody($body); } if ($detectedType === 'application/x-www-form-urlencoded') { @@ -265,29 +268,27 @@ private function parseBody(string $body, array $contentType): mixed return $parsed; } - // Return raw for other types return $body; } /** * Parse multipart form data into readable array * - * @param string $body Raw body content - * @param string $contentType Content-Type header value + * @param string $body Raw body content * * @return array */ - private function parseMultipartFormData(string $body, string $contentType): array + private function parseMultipartFormDataFromBody(string $body): array { $parsed = []; - // Extract boundary - preg_match('/boundary=(.*)$/', $contentType, $matches); - if (empty($matches[1])) { - return ['raw' => $body]; + // Extract boundary from first line + preg_match('/^--([^\r\n]+)/', $body, $boundaryMatch); + if (empty($boundaryMatch[1])) { + return ['raw' => 'Unable to parse multipart']; } - $boundary = trim($matches[1], '"'); + $boundary = $boundaryMatch[1]; $parts = explode("--$boundary", $body); foreach ($parts as $part) { From d5fd419a948b0f76d1a09c3baaea31e436df6fe0 Mon Sep 17 00:00:00 2001 From: Mufthi Ryanda Date: Tue, 7 Oct 2025 16:05:17 +0700 Subject: [PATCH 04/10] chore : remove log --- src/Libraries/ClientExternal.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Libraries/ClientExternal.php b/src/Libraries/ClientExternal.php index ddab06e..16c34fa 100644 --- a/src/Libraries/ClientExternal.php +++ b/src/Libraries/ClientExternal.php @@ -218,13 +218,11 @@ private function detectContentType(string $body): string // Check for multipart - look for Content-Disposition if (preg_match('/Content-Disposition:\s*form-data/i', $body)) { - Log::runtime()->info(['detectContentType' => 'multipart']); return 'multipart/form-data'; } // Check for URL-encoded - look for key=value pattern if (preg_match('/^[^=]+=/', $body) && !str_contains($body, "\n")) { - Log::runtime()->info(['detectContentType' => 'www']); return 'application/x-www-form-urlencoded'; } @@ -232,7 +230,6 @@ private function detectContentType(string $body): string if ($body[0] === '{' || $body[0] === '[') { json_decode($body); if (json_last_error() === JSON_ERROR_NONE) { - Log::runtime()->info(['detectContentType' => 'json']); return 'application/json'; } } From 47ab777950b235a1d5be67c2550726a4aecab7c6 Mon Sep 17 00:00:00 2001 From: Made Mas Adi Winata Date: Fri, 17 Oct 2025 14:50:01 +0700 Subject: [PATCH 05/10] set command identifier without flags arguments or options --- src/Commands/Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/Command.php b/src/Commands/Command.php index 8c038f9..abb7c7f 100644 --- a/src/Commands/Command.php +++ b/src/Commands/Command.php @@ -56,7 +56,7 @@ public function setContext(): void $context = app(Context::class); $meta = new Metadata(); $meta->task_id = $this->taskID; - $meta->identifier = $this->signature; + $meta->identifier = explode(" ", $this->signature)[0]; $context->set(Metadata::class, $meta); } } From 7cc64c3ee5b13943640fe252d4395e0502926f85 Mon Sep 17 00:00:00 2001 From: Made Mas Adi Winata Date: Tue, 28 Oct 2025 15:04:49 +0700 Subject: [PATCH 06/10] add unit test checking dtos --- tests/Dtos/DtosTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Dtos/DtosTest.php b/tests/Dtos/DtosTest.php index 4339319..cd15c5f 100644 --- a/tests/Dtos/DtosTest.php +++ b/tests/Dtos/DtosTest.php @@ -270,6 +270,8 @@ public function testDtoWithAliases(): void $this->assertIsArray($y['partner']['dog']); $this->assertEquals('Joshua', $y['partner']['dog']['name']); $this->assertEquals('Jacob', $x->siblings[0]->name); + $this->assertArrayNotHasKey('arrayOfObjectMap', $y); + $this->assertArrayNotHasKey('aliases', $y); } /** @test */ /** @runInSeparateProcess */ From 6bb81c8278c51713b49ab3c93befb63149b84455 Mon Sep 17 00:00:00 2001 From: Made Mas Adi Winata Date: Fri, 21 Nov 2025 10:11:09 +0700 Subject: [PATCH 07/10] add forwarded headers --- src/Libraries/Client.php | 29 ++++++++++++++++++++++++++++- src/Middlewares/ActivityMonitor.php | 9 +++++++++ src/Services/Metadata.php | 9 +++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Client.php b/src/Libraries/Client.php index a92e676..0286647 100644 --- a/src/Libraries/Client.php +++ b/src/Libraries/Client.php @@ -77,7 +77,7 @@ public function __construct(array $config = []) * @var Metadata $meta */ $meta = $context->get(Metadata::class); - if (!is_null($meta)) { + if ($meta !== null) { if (isset($meta->user_agent) && $meta->user_agent !== null) { $this->requestHeaders['User-Agent'] = $meta->user_agent; } @@ -141,6 +141,33 @@ public function __construct(array $config = []) if (isset($meta->req_role) && $meta->req_role !== null) { $this->requestHeaders['X-Request-Role'] = $meta->req_role; } + if (isset($meta->req_uker_supervised) && $meta->req_uker_supervised !== null) { + $this->requestHeaders['X-Request-Uker-Supervised'] = json_encode($meta->req_uker_supervised); + } + if (isset($meta->req_stell) && $meta->req_stell !== null) { + $this->requestHeaders['X-Request-Kode-Org-Jabatan'] = $meta->req_stell; + } + if (isset($meta->req_stell_tx) && $meta->req_stell_tx !== null) { + $this->requestHeaders['X-Request-Nama-Org-Jabatan'] = $meta->req_stell_tx; + } + if (isset($meta->req_kostl) && $meta->req_kostl !== null) { + $this->requestHeaders['X-Request-Kode-Cost-Center'] = $meta->req_kostl; + } + if (isset($meta->req_kostl_tx) && $meta->req_kostl_tx !== null) { + $this->requestHeaders['X-Request-Nama-Cost-Center'] = $meta->req_kostl_tx; + } + if (isset($meta->req_orgeh) && $meta->req_orgeh !== null) { + $this->requestHeaders['X-Request-Kode-Org-Unit'] = $meta->req_orgeh; + } + if (isset($meta->req_orgeh_tx) && $meta->req_orgeh_tx !== null) { + $this->requestHeaders['X-Request-Nama-Org-Unit'] = $meta->req_orgeh_tx; + } + if (isset($meta->req_level_uker) && $meta->req_level_uker !== null) { + $this->requestHeaders['X-Request-Level-Uker'] = $meta->req_level_uker; + } + if (isset($meta->req_uid) && $meta->req_uid !== null) { + $this->requestHeaders['X-Request-Uid-Las'] = $meta->req_uid; + } } } diff --git a/src/Middlewares/ActivityMonitor.php b/src/Middlewares/ActivityMonitor.php index dd56bc8..3f338a8 100644 --- a/src/Middlewares/ActivityMonitor.php +++ b/src/Middlewares/ActivityMonitor.php @@ -85,6 +85,15 @@ public function handle($request, Closure $next) $meta->version_app = $request->header('X-Version-App'); $meta->identifier = $request->getPathInfo(); $meta->req_role = $request->header('X-Request-Role'); + $meta->req_uker_supervised = json_decode($request->header('X-Request-Uker-Supervised')); + $meta->req_stell = $request->header('X-Request-Kode-Org-Jabatan'); + $meta->req_stell_tx = $request->header('X-Request-Nama-Org-Jabatan'); + $meta->req_kostl = $request->header('X-Request-Kode-Org-Cost-Center'); + $meta->req_kostl_tx = $request->header('X-Request-Nama-Org-Cost-Center'); + $meta->req_orgeh = $request->header('X-Request-Kode-Org-Unit'); + $meta->req_orgeh_tx = $request->header('X-Request-Nama-Org-Unit'); + $meta->req_level_uker = $request->header('X-Request-Level-Uker'); + $meta->req_uid = $request->header('X-Request-Uid-Las'); $this->contextService->set(Metadata::class, $meta); $this->contextService->set('method', $request->method()); diff --git a/src/Services/Metadata.php b/src/Services/Metadata.php index 7bfcde7..981ac61 100644 --- a/src/Services/Metadata.php +++ b/src/Services/Metadata.php @@ -52,4 +52,13 @@ class Metadata public ?string $path_gateway; public ?string $identifier; public ?string $req_role; + public ?array $req_uker_supervised; + public ?string $req_stell; + public ?string $req_stell_tx; + public ?string $req_kostl; + public ?string $req_kostl_tx; + public ?string $req_orgeh; + public ?string $req_orgeh_tx; + public ?string $req_level_uker; + public ?string $req_uid; } From 0f1a9c85f579ff31186849dc9f2ec684668eac89 Mon Sep 17 00:00:00 2001 From: Made Mas Adi Winata Date: Fri, 21 Nov 2025 10:20:30 +0700 Subject: [PATCH 08/10] add coverage --- tests/Libraries/ClientTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Libraries/ClientTest.php b/tests/Libraries/ClientTest.php index ae1f6d2..9468921 100644 --- a/tests/Libraries/ClientTest.php +++ b/tests/Libraries/ClientTest.php @@ -52,6 +52,16 @@ public function testCallY(): void $meta->req_nama_uker = 'test_name'; $meta->path_gateway = 'test_path'; $meta->identifier = 'test_identifier'; + $meta->req_uker_supervised = ['abc', 'def']; + $meta->req_stell = '123'; + $meta->req_stell_tx = 'abc'; + $meta->req_kostl = '123'; + $meta->req_kostl_tx = 'abc'; + $meta->req_orgeh = '123'; + $meta->req_orgeh_tx = 'abc'; + $meta->req_level_uker = 'X'; + $meta->req_uid = 'abc123'; + $meta->req_role = 'abc'; /** * @var \Mockery\MockInterface $context */ From 8139b114b46f166ca3c7ec4f0825c70868fe8dfd Mon Sep 17 00:00:00 2001 From: Made Mas Adi Winata Date: Fri, 21 Nov 2025 11:37:15 +0700 Subject: [PATCH 09/10] set requestID header with taskID if reqID not avaliable and taskID avaliable --- src/Libraries/Client.php | 2 ++ tests/Libraries/ClientTest.php | 14 +++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Client.php b/src/Libraries/Client.php index 0286647..cc733ac 100644 --- a/src/Libraries/Client.php +++ b/src/Libraries/Client.php @@ -101,6 +101,8 @@ public function __construct(array $config = []) } if (isset($meta->req_id) && $meta->req_id !== null) { $this->requestHeaders['X-Request-ID'] = $meta->req_id; + } elseif (isset($meta->task_id) && $meta->task_id !== null) { + $this->requestHeaders['X-Request-ID'] = $meta->task_id; } if (isset($meta->req_user) && $meta->req_user !== null) { $this->requestHeaders['X-Request-User'] = $meta->req_user; diff --git a/tests/Libraries/ClientTest.php b/tests/Libraries/ClientTest.php index 9468921..eb66f24 100644 --- a/tests/Libraries/ClientTest.php +++ b/tests/Libraries/ClientTest.php @@ -90,6 +90,14 @@ public function testCallX(): void 'GET', 'https://dummyjson.com/test', ); + $meta = new Metadata(); + $meta->task_id = 'abcd'; + /** + * @var \Mockery\MockInterface $context + */ + $context = Mockery::mock(Context::class); + $context->shouldReceive('get')->with(Metadata::class)->andReturn($meta); + $this->app->instance(Context::class, $context); $client = new Client(['handler' => $handlerStack]); $response = $client->call($request); $contents = $response->getBody()->getContents(); @@ -111,7 +119,11 @@ public function testCallZ(): void "message" => "welcome" ]) ); - $client = new Client(); + $mock = new MockHandler([ + new Response(200, ['Content-Type' => 'application/json'], json_encode(['status' => 'ok', 'message' => 'well done'])), + ]); + $handlerStack = new HandlerStack($mock); + $client = new Client(['handler' => $handlerStack]); $response = $client ->injectRequestHeader(['X-Powered-By' => ['Money']]) ->injectResponseHeader(['X-Server' => ['tinyurl'], 'X-Overhead' => ['true', 'allowed']]) From 56ed9926753fd91b673c1e88083e2c41d39620e9 Mon Sep 17 00:00:00 2001 From: Made Mas Adi Winata Date: Wed, 26 Nov 2025 22:27:31 +0700 Subject: [PATCH 10/10] Create .noindex --- .noindex | 1 + 1 file changed, 1 insertion(+) create mode 100644 .noindex diff --git a/.noindex b/.noindex new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.noindex @@ -0,0 +1 @@ +