$config */ public static function fromConfig(array $config): self { return new self( apiUrl: rtrim((string) ($config['api_url'] ?? ''), '/'), webUrl: rtrim((string) ($config['web_url'] ?? ''), '/'), serviceToken: filled($config['token'] ?? null) ? $config['token'] : null, timeout: (int) ($config['timeout'] ?? 10), ); } /** * The token for requests that do not carry a Pireon token of their own. */ public function serviceToken(): ?string { return $this->serviceToken; } /** * @return array{id: string, key: string} * * @throws TaskStreamException */ public function createProject(string $token, string $key, string $name, ?string $description, bool $public): array { try { $response = Http::withToken($token) ->acceptJson() ->timeout($this->timeout) ->post("{$this->apiUrl}/api/projects", array_filter([ 'key' => $key, 'name' => $name, 'description' => $description, 'isPublic' => $public, ], fn ($value) => $value !== null)); } catch (ConnectionException $e) { throw TaskStreamException::unreachable($this->apiUrl, $e); } if ($response->failed()) { // Nest answers `message` as a string, or as a list from validation. $message = $response->json('message'); throw new TaskStreamException( is_array($message) ? implode(' ', $message) : (string) ($message ?: $response->reason()), status: $response->status(), ); } return ['id' => (string) $response->json('id'), 'key' => (string) $response->json('key')]; } /** * Where people open a project's board. */ public function projectUrl(string $key): string { return "{$this->webUrl}/p/{$key}"; } }