config = $config; } public function isConfigured(): bool { return $this->baseUrl() !== '' && $this->config->getString('shayegan_sql_server', '') !== '' && $this->config->getString('shayegan_database', '') !== '' && $this->config->getString('shayegan_db_user', '') !== ''; } public function baseUrl(): string { return rtrim(trim($this->config->getString('shayegan_base_url', '')), '/'); } /** * @return array */ public function buildConfig(): array { $authUser = trim($this->config->getString('shayegan_auth_user', '')); $authPass = (string) $this->config->getString('shayegan_auth_password', ''); return [ 'Server' => $this->config->getString('shayegan_sql_server', ''), 'AllowRowSecurity' => false, 'Level' => 0, 'DBUserName' => $this->config->getString('shayegan_db_user', ''), 'DBPassword' => (string) $this->config->getString('shayegan_db_password', ''), 'DataBaseName' => $this->config->getString('shayegan_database', ''), 'AuthUser' => $authUser !== '' ? $authUser : '', 'AuthPassword' => $authUser !== '' ? $authPass : '', 'Language' => $this->config->getInt('shayegan_language', 3), ]; } /** * Read-only smoke test: Account/Get one page. * * @return array{ok:bool,message:string,http_status:?int,body:?array,timeout?:bool} */ public function healthCheck(): array { if (! $this->isConfigured()) { return [ 'ok' => false, 'message' => 'تنظیمات اتصال شایگان ناقص است (base_url / Server / DataBase / DBUser).', 'http_status' => null, 'body' => null, ]; } return $this->get('Account/Get', [], 0, 1); } /** * @param array $domain * @return array{ok:bool,message:string,http_status:?int,body:?array,timeout?:bool} */ public function get(string $endpoint, array $domain = [], int $rowStart = 0, int $rowCount = 1): array { $payload = [ 'Config' => $this->buildConfig(), // JSON object {} — empty PHP array encodes as [] and Shayegan .NET null-refs 'Domain' => $domain === [] ? new \stdClass() : $domain, ]; return $this->request('GET_STYLE', $endpoint, $payload, [ 'RowStart' => $rowStart, 'RowCount' => $rowCount, ]); } /** * @param array> $putObject * @return array{ok:bool,message:string,http_status:?int,body:?array,timeout?:bool,number:?string,guid:?string} */ public function put(string $endpoint, array $putObject): array { $payload = [ 'Config' => $this->buildConfig(), 'PutObject' => array_values($putObject), ]; $result = $this->request('PUT_STYLE', $endpoint, $payload, []); if (! empty($result['body']) && is_array($result['body'])) { $first = $this->firstResultRow($result['body']); $result['number'] = isset($first['Number']) ? (string) $first['Number'] : null; $result['guid'] = isset($first['GuId']) ? (string) $first['GuId'] : (isset($first['Guid']) ? (string) $first['Guid'] : null); } else { $result['number'] = null; $result['guid'] = null; } return $result; } /** * @param array $payload * @param array $query * @return array{ok:bool,message:string,http_status:?int,body:?array,timeout?:bool} */ private function request(string $style, string $endpoint, array $payload, array $query): array { $url = $this->endpointUrl($endpoint, $query); $timeout = max(5, $this->config->getInt('shayegan_timeout_seconds', 30)); $clientRequest = [ 'endpoint' => $endpoint, 'url' => $url, 'query' => $query, 'payload' => $this->redactPayload($payload), ]; $this->logTraffic('request', [ 'style' => $style, 'endpoint' => $endpoint, 'url' => $url, 'timeout_seconds' => $timeout, 'payload' => $clientRequest['payload'], ]); try { $response = Http::timeout($timeout) ->withHeaders(['Content-Type' => 'application/json', 'Accept' => 'application/json']) ->post($url, $payload); $status = $response->status(); $json = $response->json(); if (! is_array($json)) { $raw = (string) $response->body(); $decoded = json_decode($raw, true); $json = is_array($decoded) ? $decoded : ['raw' => Str::limit($raw, 2000)]; } $error = $this->extractErrorMessage($json); $serverResponse = $this->serverResponse($status, $error !== '' ? $error : 'OK', $json); if ($status >= 400) { $result = $this->withExchange([ 'ok' => false, 'message' => $error !== '' ? $error : ('HTTP ' . $status), 'http_status' => $status, 'body' => $json, ], $clientRequest, $serverResponse); $this->logTraffic('response', [ 'style' => $style, 'endpoint' => $endpoint, 'url' => $url, 'ok' => false, 'http_status' => $status, 'error_message' => $error, 'body' => $this->redactBodyForLog($json), ]); return $result; } if ($error !== '') { $result = $this->withExchange([ 'ok' => false, 'message' => $error, 'http_status' => $status, 'body' => $json, ], $clientRequest, $serverResponse); $this->logTraffic('response', [ 'style' => $style, 'endpoint' => $endpoint, 'url' => $url, 'ok' => false, 'http_status' => $status, 'error_message' => $error, 'body' => $this->redactBodyForLog($json), ]); return $result; } $result = $this->withExchange([ 'ok' => true, 'message' => 'OK', 'http_status' => $status, 'body' => $json, ], $clientRequest, $serverResponse); $this->logTraffic('response', [ 'style' => $style, 'endpoint' => $endpoint, 'url' => $url, 'ok' => true, 'http_status' => $status, 'error_message' => '', 'body' => $this->redactBodyForLog($json), ]); return $result; } catch (\Illuminate\Http\Client\ConnectionException $e) { Log::warning('shayegan.timeout_or_connection', [ 'endpoint' => $endpoint, 'style' => $style, 'url' => $url, 'message' => $e->getMessage(), 'payload' => $this->redactPayload($payload), ]); return $this->withExchange([ 'ok' => false, 'message' => 'timeout/connection: ' . $e->getMessage(), 'http_status' => null, 'body' => null, 'timeout' => true, ], $clientRequest, $this->serverResponse(null, 'timeout/connection: ' . $e->getMessage(), null)); } catch (\Throwable $e) { Log::warning('shayegan.request_failed', [ 'endpoint' => $endpoint, 'style' => $style, 'url' => $url, 'message' => $e->getMessage(), 'payload' => $this->redactPayload($payload), ]); return $this->withExchange([ 'ok' => false, 'message' => $e->getMessage(), 'http_status' => null, 'body' => null, ], $clientRequest, $this->serverResponse(null, $e->getMessage(), null)); } } /** * @param array $result * @return array{client_request:mixed,server_response:mixed} */ public function debugExchange(array $result): array { return [ 'client_request' => $result['client_request'] ?? null, 'server_response' => $result['server_response'] ?? null, ]; } /** * @param array $result * @param array $clientRequest * @param array $serverResponse * @return array */ private function withExchange(array $result, array $clientRequest, array $serverResponse): array { $result['client_request'] = $clientRequest; $result['server_response'] = $serverResponse; return $result; } /** * @param mixed $body * @return array */ private function serverResponse(?int $status, ?string $message, $body): array { $response = [ 'message' => $message, 'http_status' => $status, ]; if (is_array($body)) { $response['body'] = $body; } return $response; } /** * @param array $query */ private function endpointUrl(string $endpoint, array $query): string { $endpoint = ltrim(str_replace('\\', '/', $endpoint), '/'); $url = $this->baseUrl() . '/api/' . $endpoint; if ($query !== []) { $url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($query); } return $url; } /** * @param array $body */ public function extractErrorMessage(array $body): string { $top = trim((string) ($body['ErrorMessage'] ?? '')); if ($top !== '') { return $top; } $row = $this->firstResultRow($body); if ($row !== null) { $rowError = trim((string) ($row['ErrorMessage'] ?? '')); if ($rowError !== '') { return $rowError; } } return ''; } /** * @param array $body * @return array|null */ private function firstResultRow(array $body): ?array { $result = $body['Result'] ?? null; if (! is_array($result) || $result === []) { return null; } $first = isset($result[0]) && is_array($result[0]) ? $result[0] : null; if ($first !== null) { return $first; } return is_array($result) && ! isset($result[0]) ? $result : null; } /** * Strip secrets before logging / storing in admin-visible res when needed. * * @param array $payload * @return array */ public function redactPayload(array $payload): array { if (isset($payload['Config']) && is_array($payload['Config'])) { foreach (['DBPassword', 'AuthPassword'] as $key) { if (array_key_exists($key, $payload['Config']) && $payload['Config'][$key] !== '' && $payload['Config'][$key] !== null) { $payload['Config'][$key] = '***'; } } } return $payload; } /** * @param array $context */ private function logTraffic(string $phase, array $context): void { if ($this->config->getInt('shayegan_log_traffic', 1) === 0) { return; } Log::info('shayegan.http_' . $phase, $context); } /** * @param array $body * @return array */ private function redactBodyForLog(array $body): array { if (isset($body['Result']) && is_array($body['Result'])) { $count = count($body['Result']); $body['Result'] = array_slice($body['Result'], 0, 2); if ($count > 2) { $body['Result_truncated'] = $count - 2; } } return $body; } }