Files
api/services/nginx/app/classes/workfeed.php
T

329 lines
8.9 KiB
PHP

<?php
namespace classes;
require_once WD . '/modules/workfeed/workfeed_c.php';
use Exception;
use interfaces\workfeed_i;
use workfeed\workfeed_c;
class workfeed implements workfeed_i
{
public workfeed_c $config;
public function __construct()
{
$this->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(
$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($this->normalizeUrlForValidation($url), FILTER_VALIDATE_URL) === false) {
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;
}
private function normalizeUrlForValidation(string $url): string
{
if (preg_match('#^https?://#i', $url)) {
return $url;
}
return 'https://' . ltrim($url, '/');
}
/**
* @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 . ').';
}
}