config = new workfeed_c(); } /** * @throws Exception */ public function requireModuleEnabled(): void { if (!$this->config->enabled->isTrue()) { throw new Exception('The workfeed module is not enabled.'); } } /** * @throws Exception */ public function listEmployees(array $filters = []): array|object { return $this->sendApiRequest('GET', '/employees'); } /** * @throws Exception */ public function getEmployee(string $id): object { $this->requireValidIdentifier($id, 'employee id'); return $this->sendApiRequest('GET', '/employees/' . rawurlencode($id)); } /** * @throws Exception */ public function listShifts(array $filters = []): array|object { $normalizedFilters = $this->normalizeShiftFilters($filters); return $this->sendApiRequest('GET', '/shifts', $normalizedFilters); } /** * @throws Exception */ public function getShift(string $id): object { $this->requireValidIdentifier($id, 'shift id'); return $this->sendApiRequest('GET', '/shifts/' . rawurlencode($id)); } /** * @throws Exception */ public function listDepartments(array $filters = []): array|object { return $this->sendApiRequest('GET', '/departments'); } /** * @throws Exception */ private function sendApiRequest(string $method, string $path, array $query = []): array|object { $this->requireModuleEnabled(); $this->requireConfiguredApiUrl(); $this->requireConfiguredApiKey(); $companyId = $this->requireConfiguredCompanyId(); $url = $this->buildUrl( workfeed_api_url_c::normalizeApiUrlForValidation((string)$this->config->api_url->getVariableValue()), '/companies/' . rawurlencode($companyId) . '/' . ltrim($path, '/'), $query ); $headers = [ 'Accept: application/json', 'Authorization: ' . trim((string)$this->config->api_key->getVariableValue()), ]; return $this->executeJsonRequest($method, $url, $headers); } /** * @throws Exception */ private function executeJsonRequest(string $method, string $url, array $headers): array|object { $response = $this->executeRequest($method, $url, $headers); $decoded = json_decode($response['body']); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception('Invalid JSON response from Workfeed (HTTP ' . $response['status'] . ').'); } if ($response['status'] >= 400) { throw new Exception($this->extractErrorMessage($decoded, $response['status'], 'Workfeed API request failed')); } if (is_object($decoded) || is_array($decoded)) { return $decoded; } return (object)[ 'value' => $decoded, ]; } /** * @throws Exception */ private function executeRequest(string $method, string $url, array $headers, ?string $body = null): array { $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, ]); if ($body !== null) { curl_setopt($curl, CURLOPT_POSTFIELDS, $body); } $responseBody = curl_exec($curl); $statusCode = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE); $error = curl_error($curl); curl_close($curl); if ($error !== '') { throw new Exception('cURL request to Workfeed failed: ' . $error); } if ($responseBody === false) { throw new Exception('Workfeed request returned an empty response.'); } return [ 'status' => $statusCode, 'body' => (string)$responseBody, ]; } private function buildUrl(string $baseUrl, string $path = '', array $query = []): string { $url = rtrim(trim($baseUrl), '/'); if ($path !== '') { $url .= '/' . ltrim($path, '/'); } $query = array_filter($query, static function (mixed $value): bool { return $value !== null && $value !== ''; }); if ($query !== []) { $url .= '?' . http_build_query($query); } return $url; } /** * @throws Exception */ private function requireConfiguredApiUrl(): void { $url = trim((string)$this->config->api_url->getVariableValue()); if ($url === '' || filter_var(workfeed_api_url_c::normalizeApiUrlForValidation($url), FILTER_VALIDATE_URL) === false || !workfeed_api_url_c::isTrustedApiUrl($url) ) { throw new Exception('Invalid Workfeed API URL configured.'); } } /** * @throws Exception */ private function requireConfiguredApiKey(): void { if (trim((string)$this->config->api_key->getVariableValue()) === '') { throw new Exception('Invalid Workfeed API key configured.'); } } /** * @throws Exception */ private function requireConfiguredCompanyId(): string { $companyId = trim((string)$this->config->company_id->getVariableValue()); if ($companyId === '') { throw new Exception('Invalid Workfeed CompanyID configured.'); } return $companyId; } /** * @throws Exception */ private function requireValidIdentifier(string $value, string $label): void { if (trim($value) === '') { throw new Exception('Invalid ' . $label . '.'); } } private function filterAllowed(array $filters, array $allowedKeys): array { $allowed = array_flip($allowedKeys); $filtered = []; foreach ($filters as $key => $value) { if (isset($allowed[$key])) { $filtered[$key] = $value; } } return $filtered; } /** * @throws Exception */ private function normalizeShiftFilters(array $filters): array { $filtered = $this->filterAllowed($filters, [ 'startFrom', 'startTo', 'from', 'to', 'employeeID', 'employeeId', 'released', ]); if (!isset($filtered['startFrom']) && isset($filtered['from'])) { $filtered['startFrom'] = $filtered['from']; } if (!isset($filtered['startTo']) && isset($filtered['to'])) { $filtered['startTo'] = $filtered['to']; } if (!isset($filtered['employeeID']) && isset($filtered['employeeId'])) { $filtered['employeeID'] = $filtered['employeeId']; } unset($filtered['from'], $filtered['to'], $filtered['employeeId']); if (!isset($filtered['startFrom']) || trim((string)$filtered['startFrom']) === '') { throw new Exception('Workfeed shift query requires startFrom.'); } if (!isset($filtered['startTo']) || trim((string)$filtered['startTo']) === '') { throw new Exception('Workfeed shift query requires startTo.'); } if (isset($filtered['released'])) { $filtered['released'] = $this->normalizeBooleanQueryValue($filtered['released']); } return $filtered; } private function normalizeBooleanQueryValue(mixed $value): string { if (is_bool($value)) { return $value ? 'true' : 'false'; } if (is_string($value)) { $normalized = strtolower(trim($value)); if (in_array($normalized, ['1', 'true', 'yes', 'on'], true)) { return 'true'; } if (in_array($normalized, ['0', 'false', 'no', 'off'], true)) { return 'false'; } } if (is_int($value)) { return $value === 1 ? 'true' : 'false'; } return (string)$value; } private function extractErrorMessage(mixed $decoded, int $statusCode, string $fallback): string { if (is_object($decoded)) { if (isset($decoded->message) && is_string($decoded->message)) { return $decoded->message; } if (isset($decoded->error) && is_string($decoded->error)) { return $decoded->error; } } if (is_string($decoded) && trim($decoded) !== '') { return $decoded; } return $fallback . ' (HTTP ' . $statusCode . ').'; } }