> */ private array $snapshots = []; public function isConfigured(): bool { return trim((string) config('dingtalk.client_id', '')) !== '' && trim((string) config('dingtalk.client_secret', '')) !== ''; } /** * Return DingTalk's latest form component values by component name. * * @return array */ public function formValues(string $processInstanceId): array { return $this->processSnapshot($processInstanceId)['form_values']; } /** * @return array{ * form_values:array, * tasks:list>, * operation_records:list>, * status:?string, * result:?string * } */ public function processSnapshot(string $processInstanceId): array { $processInstanceId = trim($processInstanceId); if ($processInstanceId === '') { throw new DingTalkProcessInstanceException( 'DingTalk process instance ID is required.', ); } if (isset($this->snapshots[$processInstanceId])) { return $this->snapshots[$processInstanceId]; } try { $response = Http::acceptJson() ->timeout($this->timeout()) ->withHeaders([ 'x-acs-dingtalk-access-token' => $this->appAccessToken(), ]) ->get(self::PROCESS_INSTANCE_URL, [ 'processInstanceId' => $processInstanceId, ]); } catch (ConnectionException|RequestException $exception) { throw new DingTalkProcessInstanceException( 'DingTalk process instance lookup failed.', true, $exception, ); } if (! $response->successful()) { $status = $response->status(); throw new DingTalkProcessInstanceException( 'DingTalk process instance lookup returned HTTP ' . $status . '.', in_array($status, [401, 403, 429], true) || $status >= 500, ); } $result = $response->json('result'); if (! is_array($result)) { throw new DingTalkProcessInstanceException( 'DingTalk process instance response was incomplete.', ); } $values = []; foreach ((array) ($result['formComponentValues'] ?? []) as $component) { if (! is_array($component)) { continue; } $name = trim((string) ($component['name'] ?? '')); if ($name !== '') { $values[$name] = $component['value'] ?? null; } } return $this->snapshots[$processInstanceId] = [ 'form_values' => $values, 'tasks' => array_values(array_filter( (array) ($result['tasks'] ?? []), 'is_array', )), 'operation_records' => array_values(array_filter( (array) ($result['operationRecords'] ?? []), 'is_array', )), 'status' => isset($result['status']) ? (string) $result['status'] : null, 'result' => isset($result['result']) ? (string) $result['result'] : null, ]; } private function appAccessToken(): string { if ($this->accessToken !== null && time() < $this->accessTokenExpiresAt) { return $this->accessToken; } if (! $this->isConfigured()) { throw new DingTalkProcessInstanceException( 'DingTalk process API credentials are not configured.', true, ); } try { $response = Http::acceptJson() ->timeout($this->timeout()) ->get((string) config('dingtalk.app_token_url'), [ 'appkey' => (string) config('dingtalk.client_id'), 'appsecret' => (string) config('dingtalk.client_secret'), ]) ->throw(); } catch (RequestException $exception) { throw new DingTalkProcessInstanceException( 'DingTalk application token lookup failed.', true, $exception, ); } $token = trim((string) $response->json('access_token')); if ((int) $response->json('errcode', -1) !== 0 || $token === '') { throw new DingTalkProcessInstanceException( 'DingTalk application token was unavailable.', true, ); } $expiresIn = max(300, (int) $response->json('expires_in', 7200)); $this->accessToken = $token; $this->accessTokenExpiresAt = time() + max( 120, $expiresIn - self::TOKEN_SAFETY_SECONDS, ); return $token; } private function timeout(): int { return max(3, min(30, (int) config('dingtalk.timeout', 10))); } }