Files
boursecu_server/app/Services/ShayeganClient.php
T
2026-11-02 11:21:36 +03:30

394 lines
13 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
/**
* HTTP client for Shayegan Cyber Account webservice (REST JSON).
* Contract from vendor PDF: Config + Domain|PutObject; always inspect ErrorMessage.
*/
class ShayeganClient
{
/** @var ConfigService */
private $config;
public function __construct(ConfigService $config)
{
$this->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<string, mixed>
*/
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<string, mixed> $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<int, array<string, mixed>> $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<string, mixed> $payload
* @param array<string, int|string> $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<string, mixed> $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<string, mixed> $result
* @param array<string, mixed> $clientRequest
* @param array<string, mixed> $serverResponse
* @return array<string, mixed>
*/
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<string, mixed>
*/
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<string, int|string> $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<string, mixed> $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<string, mixed> $body
* @return array<string, mixed>|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<string, mixed> $payload
* @return array<string, mixed>
*/
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<string, mixed> $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<string, mixed> $body
* @return array<string, mixed>
*/
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;
}
}