diff --git a/composer.json b/composer.json index 2d60363..a94d1ce 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,14 @@ "require-dev": { "phpunit/phpunit": "^9.6.22", "rector/rector": "^2.0", - "phpstan/phpstan": "^2.1" + "phpstan/phpstan": "^2.1", + "symfony/yaml": "^7|^8", + "symfony/messenger": "^7.4", + "symfony/security-bundle": "^7.4", + "symfony/uid": "^7.4", + "symfony/string": "^7.4", + "doctrine/orm": "^3.6", + "doctrine/doctrine-bundle": "^2.18" }, "scripts": { "test": "vendor/bin/phpunit", diff --git a/phpstan.neon b/phpstan.neon index 9e92b3d..d33da4a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,3 @@ -includes: - - phpstan-baseline.neon - parameters: level: max paths: @@ -14,6 +11,3 @@ parameters: ignoreErrors: - identifier: missingType.iterableValue - - identifier: return.unusedType - - identifier: staticMethod.void - - identifier: staticMethod.alreadyNarrowedType diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..20a66cf --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + tests + + + + + + src + + + src/DependencyInjection + src/Resources + src/Enums + src/Actions/Dto + src/Provisioning/Dto + src/Provisioning/ValueObject + src/Message + + + diff --git a/src/ActionService.php b/src/ActionService.php index 8669886..ed14c4c 100644 --- a/src/ActionService.php +++ b/src/ActionService.php @@ -16,6 +16,7 @@ public function __construct( /** * @param class-string $actionClass + * @param array $input * * @throws ZabbixApiException */ @@ -32,6 +33,13 @@ public function call(string $actionClass, array $input): mixed $method = $input['method'] ?? 'get'; $params = $input['params'] ?? $input; + if (!is_string($method)) { + throw new ZabbixApiException( + 'Method must be a string', + -1, + ); + } + $actionString = sprintf('%s.%s', $base, $method); try { @@ -45,6 +53,20 @@ public function call(string $actionClass, array $input): mixed ); } - return $this->zabbixClient->call($action, $params); + if (!is_array($params)) { + throw new ZabbixApiException( + 'Params must be an array', + -1, + ); + } + + $typedParams = []; + foreach ($params as $key => $value) { + if (is_string($key)) { + $typedParams[$key] = $value; + } + } + + return $this->zabbixClient->call($action, $typedParams); } } diff --git a/src/ActionServiceInterface.php b/src/ActionServiceInterface.php index ddc8af2..ced11fc 100644 --- a/src/ActionServiceInterface.php +++ b/src/ActionServiceInterface.php @@ -7,6 +7,8 @@ interface ActionServiceInterface { /** + * @param array $input + * * @throws ZabbixApiException */ public function call(string $actionClass, array $input): mixed; diff --git a/src/Actions/Action.php b/src/Actions/Action.php index fe9c0c9..981a67b 100644 --- a/src/Actions/Action.php +++ b/src/Actions/Action.php @@ -16,6 +16,7 @@ use BytesCommerce\ZabbixApi\Actions\Dto\GetActionDto; use BytesCommerce\ZabbixApi\Enums\OutputEnum; use BytesCommerce\ZabbixApi\Enums\ZabbixAction; +use Webmozart\Assert\Assert; final class Action extends AbstractAction { @@ -32,6 +33,7 @@ public function get(GetActionDto $dto): GetActionResponseDto } $result = $this->client->call(ZabbixAction::ACTION_GET, $params); + Assert::isArray($result); return GetActionResponseDto::fromArray($result); } @@ -40,7 +42,10 @@ public function create(CreateActionDto $dto): CreateActionResponseDto { $actions = array_map($this->mapCreateAction(...), $dto->actions); - $result = $this->client->call(ZabbixAction::ACTION_CREATE, $actions); + $result = $this->client->call(ZabbixAction::ACTION_CREATE, ['actions' => $actions]); + Assert::isArray($result); + Assert::keyExists($result, 'actionids'); + Assert::isArray($result['actionids']); return new CreateActionResponseDto($result['actionids']); } @@ -49,7 +54,10 @@ public function update(UpdateActionDto $dto): UpdateActionResponseDto { $actions = array_map($this->mapUpdateAction(...), $dto->actions); - $result = $this->client->call(ZabbixAction::ACTION_UPDATE, $actions); + $result = $this->client->call(ZabbixAction::ACTION_UPDATE, ['actions' => $actions]); + Assert::isArray($result); + Assert::keyExists($result, 'actionids'); + Assert::isArray($result['actionids']); return new UpdateActionResponseDto($result['actionids']); } @@ -61,6 +69,9 @@ public function delete(DeleteActionDto $dto): DeleteActionResponseDto return new DeleteActionResponseDto(); } + /** + * @return array + */ private function mapCreateAction(CreateSingleActionDto $action): array { $data = [ @@ -96,6 +107,9 @@ private function mapCreateAction(CreateSingleActionDto $action): array return $data; } + /** + * @return array + */ private function mapUpdateAction(UpdateSingleActionDto $action): array { $data = [ diff --git a/src/Actions/Alert.php b/src/Actions/Alert.php index 229493f..fa0e224 100644 --- a/src/Actions/Alert.php +++ b/src/Actions/Alert.php @@ -8,6 +8,7 @@ use BytesCommerce\ZabbixApi\Enums\OutputEnum; use BytesCommerce\ZabbixApi\Enums\ZabbixAction; use DateTimeInterface; +use Webmozart\Assert\Assert; final class Alert extends AbstractAction { @@ -16,15 +17,23 @@ public static function getActionPrefix(): string return 'alert'; } + /** + * @param array $params + */ public function get(array $params): GetAlertResponseDto { $processedParams = $this->processParams($params); $result = $this->client->call(ZabbixAction::ALERT_GET, $processedParams); + Assert::isArray($result); return GetAlertResponseDto::fromArray($result); } + /** + * @param array $params + * @return array + */ private function processParams(array $params): array { if (!isset($params['output'])) { diff --git a/src/Actions/AuditLog.php b/src/Actions/AuditLog.php index bd06fd3..1167b78 100644 --- a/src/Actions/AuditLog.php +++ b/src/Actions/AuditLog.php @@ -8,6 +8,7 @@ use BytesCommerce\ZabbixApi\Enums\OutputEnum; use BytesCommerce\ZabbixApi\Enums\ZabbixAction; use DateTimeInterface; +use Webmozart\Assert\Assert; final class AuditLog extends AbstractAction { @@ -16,15 +17,23 @@ public static function getActionPrefix(): string return 'auditlog'; } + /** + * @param array $params + */ public function get(array $params): GetAuditLogResponseDto { $processedParams = $this->processParams($params); $result = $this->client->call(ZabbixAction::AUDITLOG_GET, $processedParams); + Assert::isArray($result); return GetAuditLogResponseDto::fromArray($result); } + /** + * @param array $params + * @return array + */ private function processParams(array $params): array { if (!isset($params['output'])) { diff --git a/src/Actions/Dashboard.php b/src/Actions/Dashboard.php index d8780cc..7c73894 100644 --- a/src/Actions/Dashboard.php +++ b/src/Actions/Dashboard.php @@ -14,6 +14,7 @@ use BytesCommerce\ZabbixApi\Actions\Dto\UpdateDashboardResponseDto; use BytesCommerce\ZabbixApi\Enums\OutputEnum; use BytesCommerce\ZabbixApi\Enums\ZabbixAction; +use Webmozart\Assert\Assert; final class Dashboard extends AbstractAction { @@ -36,20 +37,38 @@ public function get(GetDashboardDto $dto): GetDashboardResponseDto public function create(CreateDashboardDto $dto): CreateDashboardResponseDto { - $params = array_map($this->mapCreateDashboard(...), $dto->dashboards); + $params = []; + foreach ($dto->dashboards as $dashboard) { + $params[] = $this->mapCreateDashboard($dashboard); + } $result = $this->client->call(ZabbixAction::DASHBOARD_CREATE, $params); + Assert::isArray($result); + Assert::keyExists($result, 'dashboardids'); + Assert::isList($result['dashboardids']); + + /** @var list $dashboardids */ + $dashboardids = $result['dashboardids']; - return new CreateDashboardResponseDto($result['dashboardids']); + return new CreateDashboardResponseDto($dashboardids); } public function update(UpdateDashboardDto $dto): UpdateDashboardResponseDto { - $params = array_map($this->mapUpdateDashboard(...), $dto->dashboards); + $params = []; + foreach ($dto->dashboards as $dashboard) { + $params[] = $this->mapUpdateDashboard($dashboard); + } $result = $this->client->call(ZabbixAction::DASHBOARD_UPDATE, $params); + Assert::isArray($result); + Assert::keyExists($result, 'dashboardids'); + Assert::isList($result['dashboardids']); + + /** @var list $dashboardids */ + $dashboardids = $result['dashboardids']; - return new UpdateDashboardResponseDto($result['dashboardids']); + return new UpdateDashboardResponseDto($dashboardids); } public function delete(DeleteDashboardDto $dto): DeleteDashboardResponseDto @@ -59,6 +78,10 @@ public function delete(DeleteDashboardDto $dto): DeleteDashboardResponseDto return new DeleteDashboardResponseDto(); } + /** + * @param array $dashboard + * @return array + */ private function mapCreateDashboard(array $dashboard): array { $data = [ @@ -88,6 +111,10 @@ private function mapCreateDashboard(array $dashboard): array return $data; } + /** + * @param array $dashboard + * @return array + */ private function mapUpdateDashboard(array $dashboard): array { $data = [ diff --git a/src/Actions/Dto/ActionDto.php b/src/Actions/Dto/ActionDto.php index 67835aa..a999a88 100644 --- a/src/Actions/Dto/ActionDto.php +++ b/src/Actions/Dto/ActionDto.php @@ -6,6 +6,7 @@ use BytesCommerce\ZabbixApi\Enums\EventSourceEnum; use BytesCommerce\ZabbixApi\Enums\StatusEnum; +use Webmozart\Assert\Assert; final readonly class ActionDto { @@ -24,16 +25,27 @@ public function __construct( public static function fromArray(array $data): self { + Assert::string($data['actionid'] ?? null); + Assert::string($data['name'] ?? null); + Assert::integerish($data['eventsource'] ?? null); + Assert::string($data['esc_period'] ?? null); + + $status = null; + if (isset($data['status'])) { + Assert::integerish($data['status']); + $status = StatusEnum::from((int) $data['status']); + } + return new self( actionid: $data['actionid'], name: $data['name'], - eventsource: EventSourceEnum::from($data['eventsource']), + eventsource: EventSourceEnum::from((int) $data['eventsource']), esc_period: $data['esc_period'], - status: isset($data['status']) ? StatusEnum::from($data['status']) : null, - filter: $data['filter'] ?? null, - operations: $data['operations'] ?? null, - recovery_operations: $data['recovery_operations'] ?? null, - update_operations: $data['update_operations'] ?? null, + status: $status, + filter: isset($data['filter']) && is_array($data['filter']) ? $data['filter'] : null, + operations: isset($data['operations']) && is_array($data['operations']) ? $data['operations'] : null, + recovery_operations: isset($data['recovery_operations']) && is_array($data['recovery_operations']) ? $data['recovery_operations'] : null, + update_operations: isset($data['update_operations']) && is_array($data['update_operations']) ? $data['update_operations'] : null, ); } diff --git a/src/Actions/Dto/AlertDto.php b/src/Actions/Dto/AlertDto.php index f181802..a2232e0 100644 --- a/src/Actions/Dto/AlertDto.php +++ b/src/Actions/Dto/AlertDto.php @@ -4,6 +4,8 @@ namespace BytesCommerce\ZabbixApi\Actions\Dto; +use Webmozart\Assert\Assert; + final readonly class AlertDto { public function __construct( @@ -27,22 +29,36 @@ public function __construct( public static function fromArray(array $data): self { + Assert::string($data['alertid'] ?? null); + Assert::string($data['actionid'] ?? null); + Assert::string($data['eventid'] ?? null); + Assert::string($data['userid'] ?? null); + Assert::integerish($data['clock'] ?? null); + Assert::integerish($data['mediatypeid'] ?? null); + Assert::string($data['sendto'] ?? null); + Assert::string($data['subject'] ?? null); + Assert::string($data['message'] ?? null); + Assert::integerish($data['status'] ?? null); + Assert::integerish($data['retries'] ?? null); + Assert::string($data['error'] ?? null); + Assert::integerish($data['esc_step'] ?? null); + return new self( alertid: $data['alertid'], actionid: $data['actionid'], eventid: $data['eventid'], userid: $data['userid'], - clock: $data['clock'], - mediatypeid: $data['mediatypeid'], + clock: (int) $data['clock'], + mediatypeid: (int) $data['mediatypeid'], sendto: $data['sendto'], subject: $data['subject'], message: $data['message'], - status: $data['status'], - retries: $data['retries'], + status: (int) $data['status'], + retries: (int) $data['retries'], error: $data['error'], - esc_step: $data['esc_step'], - alerttype: $data['alerttype'] ?? null, - p_eventid: $data['p_eventid'] ?? null, + esc_step: (int) $data['esc_step'], + alerttype: isset($data['alerttype']) && is_string($data['alerttype']) ? $data['alerttype'] : null, + p_eventid: isset($data['p_eventid']) && is_string($data['p_eventid']) ? $data['p_eventid'] : null, ); } diff --git a/src/Actions/Dto/AuditLogDto.php b/src/Actions/Dto/AuditLogDto.php index 1174cca..62ee229 100644 --- a/src/Actions/Dto/AuditLogDto.php +++ b/src/Actions/Dto/AuditLogDto.php @@ -4,6 +4,8 @@ namespace BytesCommerce\ZabbixApi\Actions\Dto; +use Webmozart\Assert\Assert; + final readonly class AuditLogDto { public function __construct( @@ -22,17 +24,25 @@ public function __construct( public static function fromArray(array $data): self { + Assert::string($data['auditid'] ?? null); + Assert::string($data['userid'] ?? null); + Assert::integerish($data['clock'] ?? null); + Assert::string($data['action'] ?? null); + Assert::string($data['resourcetype'] ?? null); + Assert::string($data['resourceid'] ?? null); + Assert::string($data['resourcename'] ?? null); + return new self( auditid: $data['auditid'], userid: $data['userid'], - clock: $data['clock'], + clock: (int) $data['clock'], action: $data['action'], resourcetype: $data['resourcetype'], resourceid: $data['resourceid'], resourcename: $data['resourcename'], - details: $data['details'] ?? null, - ip: $data['ip'] ?? null, - resource_cuid: $data['resource_cuid'] ?? null, + details: isset($data['details']) && is_string($data['details']) ? $data['details'] : null, + ip: isset($data['ip']) && is_string($data['ip']) ? $data['ip'] : null, + resource_cuid: isset($data['resource_cuid']) && is_string($data['resource_cuid']) ? $data['resource_cuid'] : null, ); } diff --git a/src/Actions/Dto/CreateDashboardDto.php b/src/Actions/Dto/CreateDashboardDto.php index 60185bc..2fa10ba 100644 --- a/src/Actions/Dto/CreateDashboardDto.php +++ b/src/Actions/Dto/CreateDashboardDto.php @@ -7,7 +7,7 @@ final readonly class CreateDashboardDto { /** - * @param list, view_mode?: string}>}> $dashboards + * @param list> $dashboards */ public function __construct( public array $dashboards, @@ -15,7 +15,7 @@ public function __construct( } /** - * @return list, view_mode?: string}>}> + * @return list> */ public function getDashboards(): array { diff --git a/src/Actions/Dto/DashboardDto.php b/src/Actions/Dto/DashboardDto.php index f3320cd..d64a94f 100644 --- a/src/Actions/Dto/DashboardDto.php +++ b/src/Actions/Dto/DashboardDto.php @@ -4,6 +4,8 @@ namespace BytesCommerce\ZabbixApi\Actions\Dto; +use Webmozart\Assert\Assert; + final readonly class DashboardDto { /** @@ -22,17 +24,43 @@ public function __construct( public static function fromArray(array $data): self { - $pages = isset($data['pages']) && is_array($data['pages']) - ? array_map(DashboardPageDto::fromArray(...), $data['pages']) - : []; + Assert::string($data['dashboardid'] ?? null); + Assert::string($data['name'] ?? null); + + $pages = []; + if (isset($data['pages']) && is_array($data['pages'])) { + foreach ($data['pages'] as $pageData) { + if (is_array($pageData)) { + $pages[] = DashboardPageDto::fromArray($pageData); + } + } + } + + $private = null; + if (isset($data['private'])) { + Assert::integerish($data['private']); + $private = (int) $data['private']; + } + + $displayPeriod = null; + if (isset($data['display_period'])) { + Assert::integerish($data['display_period']); + $displayPeriod = (int) $data['display_period']; + } + + $autoStart = null; + if (isset($data['auto_start'])) { + Assert::integerish($data['auto_start']); + $autoStart = (int) $data['auto_start']; + } return new self( dashboardid: $data['dashboardid'], name: $data['name'], - private: isset($data['private']) ? (int) $data['private'] : null, - userid: $data['userid'] ?? null, - display_period: isset($data['display_period']) ? (int) $data['display_period'] : null, - auto_start: isset($data['auto_start']) ? (int) $data['auto_start'] : null, + private: $private, + userid: isset($data['userid']) && is_string($data['userid']) ? $data['userid'] : null, + display_period: $displayPeriod, + auto_start: $autoStart, pages: $pages, ); } diff --git a/src/Actions/Dto/DashboardPageDto.php b/src/Actions/Dto/DashboardPageDto.php index 4b7ecae..c87c8a2 100644 --- a/src/Actions/Dto/DashboardPageDto.php +++ b/src/Actions/Dto/DashboardPageDto.php @@ -19,14 +19,29 @@ public function __construct( public static function fromArray(array $data): self { - $widgets = isset($data['widgets']) && is_array($data['widgets']) - ? array_map(DashboardWidgetDto::fromArray(...), $data['widgets']) - : []; + $widgets = []; + if (isset($data['widgets']) && is_array($data['widgets'])) { + foreach ($data['widgets'] as $widgetData) { + if (is_array($widgetData)) { + $widgets[] = DashboardWidgetDto::fromArray($widgetData); + } + } + } + + $displayPeriod = null; + if (isset($data['display_period'])) { + $displayPeriod = is_int($data['display_period']) ? $data['display_period'] : null; + } + + $sortorder = null; + if (isset($data['sortorder'])) { + $sortorder = is_int($data['sortorder']) ? $data['sortorder'] : null; + } return new self( - name: $data['name'] ?? null, - display_period: isset($data['display_period']) ? (int) $data['display_period'] : null, - sortorder: isset($data['sortorder']) ? (int) $data['sortorder'] : null, + name: isset($data['name']) && is_string($data['name']) ? $data['name'] : null, + display_period: $displayPeriod, + sortorder: $sortorder, widgets: $widgets, ); } diff --git a/src/Actions/Dto/DashboardWidgetDto.php b/src/Actions/Dto/DashboardWidgetDto.php index 98a1120..a802b5b 100644 --- a/src/Actions/Dto/DashboardWidgetDto.php +++ b/src/Actions/Dto/DashboardWidgetDto.php @@ -4,6 +4,8 @@ namespace BytesCommerce\ZabbixApi\Actions\Dto; +use Webmozart\Assert\Assert; + final readonly class DashboardWidgetDto { /** @@ -23,18 +25,59 @@ public function __construct( public static function fromArray(array $data): self { + Assert::string($data['type'] ?? null); + + $x = null; + if (isset($data['x'])) { + Assert::integerish($data['x']); + $x = (int) $data['x']; + } + + $y = null; + if (isset($data['y'])) { + Assert::integerish($data['y']); + $y = (int) $data['y']; + } + + $width = null; + if (isset($data['width'])) { + Assert::integerish($data['width']); + $width = (int) $data['width']; + } + + $height = null; + if (isset($data['height'])) { + Assert::integerish($data['height']); + $height = (int) $data['height']; + } + return new self( type: $data['type'], - name: $data['name'] ?? null, - x: isset($data['x']) ? (int) $data['x'] : null, - y: isset($data['y']) ? (int) $data['y'] : null, - width: isset($data['width']) ? (int) $data['width'] : null, - height: isset($data['height']) ? (int) $data['height'] : null, - fields: $data['fields'] ?? null, - view_mode: $data['view_mode'] ?? null, + name: isset($data['name']) && is_string($data['name']) ? $data['name'] : null, + x: $x, + y: $y, + width: $width, + height: $height, + fields: isset($data['fields']) && is_array($data['fields']) ? self::normalizeFields($data['fields']) : null, + view_mode: isset($data['view_mode']) && is_string($data['view_mode']) ? $data['view_mode'] : null, ); } + /** + * @param array $fields + * @return array + */ + private static function normalizeFields(array $fields): array + { + $normalized = []; + foreach ($fields as $key => $value) { + if (is_string($key)) { + $normalized[$key] = $value; + } + } + return $normalized; + } + public function getType(): string { return $this->type; diff --git a/src/Actions/Dto/EventDto.php b/src/Actions/Dto/EventDto.php index e05dbf2..6b3b9d3 100644 --- a/src/Actions/Dto/EventDto.php +++ b/src/Actions/Dto/EventDto.php @@ -4,6 +4,8 @@ namespace BytesCommerce\ZabbixApi\Actions\Dto; +use Webmozart\Assert\Assert; + final readonly class EventDto { public function __construct( @@ -25,20 +27,35 @@ public function __construct( public static function fromArray(array $data): self { + Assert::string($data['eventid'] ?? null); + Assert::integerish($data['source'] ?? null); + Assert::integerish($data['object'] ?? null); + Assert::integerish($data['objectid'] ?? null); + Assert::integerish($data['clock'] ?? null); + Assert::integerish($data['value'] ?? null); + Assert::integerish($data['acknowledged'] ?? null); + Assert::integerish($data['ns'] ?? null); + + $severity = null; + if (isset($data['severity'])) { + Assert::integerish($data['severity']); + $severity = (int) $data['severity']; + } + return new self( eventid: $data['eventid'], - source: $data['source'], - object: $data['object'], - objectid: $data['objectid'], - clock: $data['clock'], - value: $data['value'], - acknowledged: $data['acknowledged'], - ns: $data['ns'], - name: $data['name'] ?? null, - severity: $data['severity'] ?? null, - acknowledges: $data['acknowledges'] ?? null, - hosts: $data['hosts'] ?? null, - tags: $data['tags'] ?? null, + source: (int) $data['source'], + object: (int) $data['object'], + objectid: (int) $data['objectid'], + clock: (int) $data['clock'], + value: (int) $data['value'], + acknowledged: (int) $data['acknowledged'], + ns: (int) $data['ns'], + name: isset($data['name']) && is_string($data['name']) ? $data['name'] : null, + severity: $severity, + acknowledges: isset($data['acknowledges']) && is_array($data['acknowledges']) ? $data['acknowledges'] : null, + hosts: isset($data['hosts']) && is_array($data['hosts']) ? $data['hosts'] : null, + tags: isset($data['tags']) && is_array($data['tags']) ? $data['tags'] : null, ); } diff --git a/src/Actions/Dto/GetActionResponseDto.php b/src/Actions/Dto/GetActionResponseDto.php index 04b435f..61208d6 100644 --- a/src/Actions/Dto/GetActionResponseDto.php +++ b/src/Actions/Dto/GetActionResponseDto.php @@ -7,7 +7,7 @@ final readonly class GetActionResponseDto { /** - * @param ActionDto[] $actions + * @param list $actions */ public function __construct( public array $actions, @@ -16,8 +16,13 @@ public function __construct( public static function fromArray(array $data): self { - return new self( - actions: array_map(ActionDto::fromArray(...), $data), - ); + $actions = []; + foreach ($data as $item) { + if (is_array($item)) { + $actions[] = ActionDto::fromArray($item); + } + } + + return new self(actions: $actions); } } diff --git a/src/Actions/Dto/GetAlertResponseDto.php b/src/Actions/Dto/GetAlertResponseDto.php index d7ea685..d2a47b2 100644 --- a/src/Actions/Dto/GetAlertResponseDto.php +++ b/src/Actions/Dto/GetAlertResponseDto.php @@ -7,7 +7,7 @@ final readonly class GetAlertResponseDto { /** - * @param AlertDto[] $alerts + * @param list $alerts */ public function __construct( public array $alerts, @@ -16,8 +16,13 @@ public function __construct( public static function fromArray(array $data): self { - return new self( - alerts: array_map(AlertDto::fromArray(...), $data), - ); + $alerts = []; + foreach ($data as $item) { + if (is_array($item)) { + $alerts[] = AlertDto::fromArray($item); + } + } + + return new self(alerts: $alerts); } } diff --git a/src/Actions/Dto/GetAuditLogResponseDto.php b/src/Actions/Dto/GetAuditLogResponseDto.php index a7ba656..086789f 100644 --- a/src/Actions/Dto/GetAuditLogResponseDto.php +++ b/src/Actions/Dto/GetAuditLogResponseDto.php @@ -7,7 +7,7 @@ final readonly class GetAuditLogResponseDto { /** - * @param AuditLogDto[] $auditlogs + * @param list $auditlogs */ public function __construct( public array $auditlogs, @@ -16,8 +16,13 @@ public function __construct( public static function fromArray(array $data): self { - return new self( - auditlogs: array_map(AuditLogDto::fromArray(...), $data), - ); + $auditlogs = []; + foreach ($data as $item) { + if (is_array($item)) { + $auditlogs[] = AuditLogDto::fromArray($item); + } + } + + return new self(auditlogs: $auditlogs); } } diff --git a/src/Actions/Dto/GetDashboardResponseDto.php b/src/Actions/Dto/GetDashboardResponseDto.php index e2b481d..3aaea33 100644 --- a/src/Actions/Dto/GetDashboardResponseDto.php +++ b/src/Actions/Dto/GetDashboardResponseDto.php @@ -16,9 +16,14 @@ public function __construct( public static function fromArray(array $data): self { - return new self( - dashboards: array_map(DashboardDto::fromArray(...), $data), - ); + $dashboards = []; + foreach ($data as $item) { + if (is_array($item)) { + $dashboards[] = DashboardDto::fromArray($item); + } + } + + return new self(dashboards: $dashboards); } /** diff --git a/src/Actions/Dto/GetEventResponseDto.php b/src/Actions/Dto/GetEventResponseDto.php index 658e855..b6d150d 100644 --- a/src/Actions/Dto/GetEventResponseDto.php +++ b/src/Actions/Dto/GetEventResponseDto.php @@ -7,7 +7,7 @@ final readonly class GetEventResponseDto { /** - * @param EventDto[] $events + * @param list $events */ public function __construct( public array $events, @@ -16,8 +16,13 @@ public function __construct( public static function fromArray(array $data): self { - return new self( - events: array_map(EventDto::fromArray(...), $data), - ); + $events = []; + foreach ($data as $item) { + if (is_array($item)) { + $events[] = EventDto::fromArray($item); + } + } + + return new self(events: $events); } } diff --git a/src/Actions/Dto/GetGraphResponseDto.php b/src/Actions/Dto/GetGraphResponseDto.php index a289454..6a2734b 100644 --- a/src/Actions/Dto/GetGraphResponseDto.php +++ b/src/Actions/Dto/GetGraphResponseDto.php @@ -16,9 +16,14 @@ public function __construct( public static function fromArray(array $data): self { - return new self( - graphs: array_map(GraphDto::fromArray(...), $data), - ); + $graphs = []; + foreach ($data as $item) { + if (is_array($item)) { + $graphs[] = GraphDto::fromArray($item); + } + } + + return new self(graphs: $graphs); } /** diff --git a/src/Actions/Dto/GetHistoryResponseDto.php b/src/Actions/Dto/GetHistoryResponseDto.php index 60255a7..6ba9c87 100644 --- a/src/Actions/Dto/GetHistoryResponseDto.php +++ b/src/Actions/Dto/GetHistoryResponseDto.php @@ -7,7 +7,7 @@ final readonly class GetHistoryResponseDto { /** - * @param HistoryDto[] $history + * @param list $history */ public function __construct( public array $history, @@ -16,13 +16,18 @@ public function __construct( public static function fromArray(array $data): self { - return new self( - history: array_map(HistoryDto::fromArray(...), $data), - ); + $history = []; + foreach ($data as $item) { + if (is_array($item)) { + $history[] = HistoryDto::fromArray($item); + } + } + + return new self(history: $history); } /** - * @return HistoryDto[] + * @return list */ public function getHistory(): array { diff --git a/src/Actions/Dto/GetHostGroupResponseDto.php b/src/Actions/Dto/GetHostGroupResponseDto.php index 6ee723f..641d41f 100644 --- a/src/Actions/Dto/GetHostGroupResponseDto.php +++ b/src/Actions/Dto/GetHostGroupResponseDto.php @@ -16,9 +16,14 @@ public function __construct( public static function fromArray(array $data): self { - return new self( - hostGroups: array_map(HostGroupDto::fromArray(...), $data), - ); + $hostGroups = []; + foreach ($data as $item) { + if (is_array($item)) { + $hostGroups[] = HostGroupDto::fromArray($item); + } + } + + return new self(hostGroups: $hostGroups); } /** diff --git a/src/Actions/Dto/GetHostResponseDto.php b/src/Actions/Dto/GetHostResponseDto.php index badffad..2be0c6d 100644 --- a/src/Actions/Dto/GetHostResponseDto.php +++ b/src/Actions/Dto/GetHostResponseDto.php @@ -7,7 +7,7 @@ final readonly class GetHostResponseDto { /** - * @param HostDto[] $hosts + * @param list $hosts */ public function __construct( public array $hosts, @@ -16,8 +16,13 @@ public function __construct( public static function fromArray(array $data): self { - return new self( - hosts: array_map(HostDto::fromArray(...), $data), - ); + $hosts = []; + foreach ($data as $item) { + if (is_array($item)) { + $hosts[] = HostDto::fromArray($item); + } + } + + return new self(hosts: $hosts); } } diff --git a/src/Actions/Dto/GetItemResponseDto.php b/src/Actions/Dto/GetItemResponseDto.php index 9b24b9d..4b86626 100644 --- a/src/Actions/Dto/GetItemResponseDto.php +++ b/src/Actions/Dto/GetItemResponseDto.php @@ -7,7 +7,7 @@ final readonly class GetItemResponseDto { /** - * @param ItemDto[] $items + * @param list $items */ public function __construct( public array $items, @@ -16,8 +16,13 @@ public function __construct( public static function fromArray(array $data): self { - return new self( - items: array_map(ItemDto::fromArray(...), $data), - ); + $items = []; + foreach ($data as $item) { + if (is_array($item)) { + $items[] = ItemDto::fromArray($item); + } + } + + return new self(items: $items); } } diff --git a/src/Actions/Dto/GetObjectsHostGroupResponseDto.php b/src/Actions/Dto/GetObjectsHostGroupResponseDto.php index f871154..bb574ca 100644 --- a/src/Actions/Dto/GetObjectsHostGroupResponseDto.php +++ b/src/Actions/Dto/GetObjectsHostGroupResponseDto.php @@ -16,9 +16,14 @@ public function __construct( public static function fromArray(array $data): self { - return new self( - hostGroups: array_map(HostGroupDto::fromArray(...), $data), - ); + $hostGroups = []; + foreach ($data as $item) { + if (is_array($item)) { + $hostGroups[] = HostGroupDto::fromArray($item); + } + } + + return new self(hostGroups: $hostGroups); } /** diff --git a/src/Actions/Dto/GetTriggerResponseDto.php b/src/Actions/Dto/GetTriggerResponseDto.php index a18b0e7..3e2e8fe 100644 --- a/src/Actions/Dto/GetTriggerResponseDto.php +++ b/src/Actions/Dto/GetTriggerResponseDto.php @@ -7,7 +7,7 @@ final readonly class GetTriggerResponseDto { /** - * @param TriggerDto[] $triggers + * @param list $triggers */ public function __construct( public array $triggers, @@ -16,8 +16,13 @@ public function __construct( public static function fromArray(array $data): self { - return new self( - triggers: array_map(TriggerDto::fromArray(...), $data), - ); + $triggers = []; + foreach ($data as $item) { + if (is_array($item)) { + $triggers[] = TriggerDto::fromArray($item); + } + } + + return new self(triggers: $triggers); } } diff --git a/src/Actions/Dto/GraphDto.php b/src/Actions/Dto/GraphDto.php index 05e25fd..a0090a9 100644 --- a/src/Actions/Dto/GraphDto.php +++ b/src/Actions/Dto/GraphDto.php @@ -4,6 +4,8 @@ namespace BytesCommerce\ZabbixApi\Actions\Dto; +use Webmozart\Assert\Assert; + final readonly class GraphDto { /** @@ -33,28 +35,114 @@ public function __construct( public static function fromArray(array $data): self { - $gitems = isset($data['gitems']) && is_array($data['gitems']) - ? array_map(GraphItemDto::fromArray(...), $data['gitems']) - : []; + Assert::string($data['graphid'] ?? null); + Assert::string($data['name'] ?? null); + + $gitems = []; + if (isset($data['gitems']) && is_array($data['gitems'])) { + foreach ($data['gitems'] as $itemData) { + if (is_array($itemData)) { + $gitems[] = GraphItemDto::fromArray($itemData); + } + } + } + + $width = null; + if (isset($data['width'])) { + Assert::integerish($data['width']); + $width = (int) $data['width']; + } + + $height = null; + if (isset($data['height'])) { + Assert::integerish($data['height']); + $height = (int) $data['height']; + } + + $yaxismin = null; + if (isset($data['yaxismin'])) { + Assert::numeric($data['yaxismin']); + $yaxismin = (float) $data['yaxismin']; + } + + $yaxismax = null; + if (isset($data['yaxismax'])) { + Assert::numeric($data['yaxismax']); + $yaxismax = (float) $data['yaxismax']; + } + + $showWorkPeriod = null; + if (isset($data['show_work_period'])) { + Assert::integerish($data['show_work_period']); + $showWorkPeriod = (int) $data['show_work_period']; + } + + $showTriggers = null; + if (isset($data['show_triggers'])) { + Assert::integerish($data['show_triggers']); + $showTriggers = (int) $data['show_triggers']; + } + + $graphtype = null; + if (isset($data['graphtype'])) { + Assert::integerish($data['graphtype']); + $graphtype = (int) $data['graphtype']; + } + + $showLegend = null; + if (isset($data['show_legend'])) { + Assert::integerish($data['show_legend']); + $showLegend = (int) $data['show_legend']; + } + + $show3d = null; + if (isset($data['show_3d'])) { + Assert::integerish($data['show_3d']); + $show3d = (int) $data['show_3d']; + } + + $percentLeft = null; + if (isset($data['percent_left'])) { + Assert::numeric($data['percent_left']); + $percentLeft = (float) $data['percent_left']; + } + + $percentRight = null; + if (isset($data['percent_right'])) { + Assert::numeric($data['percent_right']); + $percentRight = (float) $data['percent_right']; + } + + $yminType = null; + if (isset($data['ymin_type'])) { + Assert::integerish($data['ymin_type']); + $yminType = (int) $data['ymin_type']; + } + + $ymaxType = null; + if (isset($data['ymax_type'])) { + Assert::integerish($data['ymax_type']); + $ymaxType = (int) $data['ymax_type']; + } return new self( graphid: $data['graphid'], name: $data['name'], - width: isset($data['width']) ? (int) $data['width'] : null, - height: isset($data['height']) ? (int) $data['height'] : null, - yaxismin: isset($data['yaxismin']) ? (float) $data['yaxismin'] : null, - yaxismax: isset($data['yaxismax']) ? (float) $data['yaxismax'] : null, - show_work_period: isset($data['show_work_period']) ? (int) $data['show_work_period'] : null, - show_triggers: isset($data['show_triggers']) ? (int) $data['show_triggers'] : null, - graphtype: isset($data['graphtype']) ? (int) $data['graphtype'] : null, - show_legend: isset($data['show_legend']) ? (int) $data['show_legend'] : null, - show_3d: isset($data['show_3d']) ? (int) $data['show_3d'] : null, - percent_left: isset($data['percent_left']) ? (float) $data['percent_left'] : null, - percent_right: isset($data['percent_right']) ? (float) $data['percent_right'] : null, - ymin_type: isset($data['ymin_type']) ? (int) $data['ymin_type'] : null, - ymax_type: isset($data['ymax_type']) ? (int) $data['ymax_type'] : null, - ymin_itemid: $data['ymin_itemid'] ?? null, - ymax_itemid: $data['ymax_itemid'] ?? null, + width: $width, + height: $height, + yaxismin: $yaxismin, + yaxismax: $yaxismax, + show_work_period: $showWorkPeriod, + show_triggers: $showTriggers, + graphtype: $graphtype, + show_legend: $showLegend, + show_3d: $show3d, + percent_left: $percentLeft, + percent_right: $percentRight, + ymin_type: $yminType, + ymax_type: $ymaxType, + ymin_itemid: isset($data['ymin_itemid']) && is_string($data['ymin_itemid']) ? $data['ymin_itemid'] : null, + ymax_itemid: isset($data['ymax_itemid']) && is_string($data['ymax_itemid']) ? $data['ymax_itemid'] : null, gitems: $gitems, ); } diff --git a/src/Actions/Dto/GraphItemDto.php b/src/Actions/Dto/GraphItemDto.php index a533f73..251c367 100644 --- a/src/Actions/Dto/GraphItemDto.php +++ b/src/Actions/Dto/GraphItemDto.php @@ -4,6 +4,8 @@ namespace BytesCommerce\ZabbixApi\Actions\Dto; +use Webmozart\Assert\Assert; + final readonly class GraphItemDto { public function __construct( @@ -20,15 +22,48 @@ public function __construct( public static function fromArray(array $data): self { + Assert::string($data['gitemid'] ?? null); + Assert::string($data['itemid'] ?? null); + + $drawtype = null; + if (isset($data['drawtype'])) { + Assert::integerish($data['drawtype']); + $drawtype = (int) $data['drawtype']; + } + + $sortorder = null; + if (isset($data['sortorder'])) { + Assert::integerish($data['sortorder']); + $sortorder = (int) $data['sortorder']; + } + + $yaxisside = null; + if (isset($data['yaxisside'])) { + Assert::integerish($data['yaxisside']); + $yaxisside = (int) $data['yaxisside']; + } + + $calcFnc = null; + if (isset($data['calc_fnc'])) { + Assert::integerish($data['calc_fnc']); + $calcFnc = (int) $data['calc_fnc']; + } + + $type = null; + if (isset($data['type'])) { + Assert::integerish($data['type']); + $type = (int) $data['type']; + } + return new self( gitemid: $data['gitemid'], itemid: $data['itemid'], - drawtype: isset($data['drawtype']) ? (int) $data['drawtype'] : null, - sortorder: isset($data['sortorder']) ? (int) $data['sortorder'] : null, - color: $data['color'] ?? null, - yaxisside: isset($data['yaxisside']) ? (int) $data['yaxisside'] : null, - calc_fnc: isset($data['calc_fnc']) ? (int) $data['calc_fnc'] : null, - type: isset($data['type']) ? (int) $data['type'] : null, + drawtype: $drawtype, + sortorder: $sortorder, + color: isset($data['color']) && is_string($data['color']) ? $data['color'] : null, + yaxisside: $yaxisside, + calc_fnc: $calcFnc, + type: $type, ); } diff --git a/src/Actions/Dto/HistoryDto.php b/src/Actions/Dto/HistoryDto.php index 2098d7d..f8327e1 100644 --- a/src/Actions/Dto/HistoryDto.php +++ b/src/Actions/Dto/HistoryDto.php @@ -4,7 +4,7 @@ namespace BytesCommerce\ZabbixApi\Actions\Dto; -use BytesCommerce\ZabbixApi\Enums\HistoryTypeEnum; +use Webmozart\Assert\Assert; final readonly class HistoryDto { @@ -23,16 +23,44 @@ public function __construct( public static function fromArray(array $data): self { + Assert::string($data['itemid'] ?? null); + Assert::integerish($data['clock'] ?? null); + Assert::string($data['value'] ?? null); + + $ns = null; + if (isset($data['ns'])) { + Assert::integerish($data['ns']); + $ns = (int) $data['ns']; + } + + $timestamp = null; + if (isset($data['timestamp'])) { + Assert::integerish($data['timestamp']); + $timestamp = (int) $data['timestamp']; + } + + $logeventid = null; + if (isset($data['logeventid'])) { + Assert::integerish($data['logeventid']); + $logeventid = (int) $data['logeventid']; + } + + $severity = null; + if (isset($data['severity'])) { + Assert::integerish($data['severity']); + $severity = (int) $data['severity']; + } + return new self( itemid: $data['itemid'], clock: (int) $data['clock'], value: $data['value'], - ns: isset($data['ns']) ? (int) $data['ns'] : null, - timestamp: isset($data['timestamp']) ? (int) $data['timestamp'] : null, - logeventid: isset($data['logeventid']) ? (int) $data['logeventid'] : null, - severity: isset($data['severity']) ? (int) $data['severity'] : null, - source: $data['source'] ?? null, - eventid: $data['eventid'] ?? null, + ns: $ns, + timestamp: $timestamp, + logeventid: $logeventid, + severity: $severity, + source: isset($data['source']) && is_string($data['source']) ? $data['source'] : null, + eventid: isset($data['eventid']) && is_string($data['eventid']) ? $data['eventid'] : null, ); } diff --git a/src/Actions/Dto/HostDto.php b/src/Actions/Dto/HostDto.php index 4bca768..28fad3b 100644 --- a/src/Actions/Dto/HostDto.php +++ b/src/Actions/Dto/HostDto.php @@ -5,6 +5,7 @@ namespace BytesCommerce\ZabbixApi\Actions\Dto; use BytesCommerce\ZabbixApi\Enums\StatusEnum; +use Webmozart\Assert\Assert; final readonly class HostDto { @@ -23,16 +24,20 @@ public function __construct( public static function fromArray(array $data): self { + Assert::string($data['hostid'] ?? null); + Assert::string($data['host'] ?? null); + Assert::integerish($data['status'] ?? null); + return new self( hostid: $data['hostid'], host: $data['host'], - name: $data['name'] ?? null, - status: StatusEnum::from($data['status']), - interfaces: $data['interfaces'] ?? null, - groups: $data['groups'] ?? null, - templates: $data['templates'] ?? null, - macros: $data['macros'] ?? null, - tags: $data['tags'] ?? null, + name: isset($data['name']) && is_string($data['name']) ? $data['name'] : null, + status: StatusEnum::from((int) $data['status']), + interfaces: isset($data['interfaces']) && is_array($data['interfaces']) ? $data['interfaces'] : null, + groups: isset($data['groups']) && is_array($data['groups']) ? $data['groups'] : null, + templates: isset($data['templates']) && is_array($data['templates']) ? $data['templates'] : null, + macros: isset($data['macros']) && is_array($data['macros']) ? $data['macros'] : null, + tags: isset($data['tags']) && is_array($data['tags']) ? $data['tags'] : null, ); } diff --git a/src/Actions/Dto/HostGroupDto.php b/src/Actions/Dto/HostGroupDto.php index a047deb..10b5300 100644 --- a/src/Actions/Dto/HostGroupDto.php +++ b/src/Actions/Dto/HostGroupDto.php @@ -4,6 +4,8 @@ namespace BytesCommerce\ZabbixApi\Actions\Dto; +use Webmozart\Assert\Assert; + final readonly class HostGroupDto { public function __construct( @@ -16,11 +18,26 @@ public function __construct( public static function fromArray(array $data): self { + Assert::string($data['groupid'] ?? null); + Assert::string($data['name'] ?? null); + + $flags = null; + if (isset($data['flags'])) { + Assert::integerish($data['flags']); + $flags = (int) $data['flags']; + } + + $internal = null; + if (isset($data['internal'])) { + Assert::integerish($data['internal']); + $internal = (int) $data['internal']; + } + return new self( groupid: $data['groupid'], name: $data['name'], - flags: isset($data['flags']) ? (int) $data['flags'] : null, - internal: isset($data['internal']) ? (int) $data['internal'] : null, + flags: $flags, + internal: $internal, ); } diff --git a/src/Actions/Dto/ItemDto.php b/src/Actions/Dto/ItemDto.php index 828faf8..d2f7408 100644 --- a/src/Actions/Dto/ItemDto.php +++ b/src/Actions/Dto/ItemDto.php @@ -4,9 +4,10 @@ namespace BytesCommerce\ZabbixApi\Actions\Dto; +use BytesCommerce\ZabbixApi\Enums\ItemTypeEnum; use BytesCommerce\ZabbixApi\Enums\StatusEnum; -use BytesCommerce\ZabbixApi\ItemTypeEnum; -use BytesCommerce\ZabbixApi\ValueTypeEnum; +use BytesCommerce\ZabbixApi\Enums\ValueTypeEnum; +use Webmozart\Assert\Assert; final readonly class ItemDto { @@ -27,18 +28,32 @@ public function __construct( public static function fromArray(array $data): self { + Assert::string($data['itemid'] ?? null); + Assert::string($data['name'] ?? null); + Assert::string($data['key_'] ?? null); + Assert::string($data['hostid'] ?? null); + Assert::integerish($data['type'] ?? null); + Assert::integerish($data['value_type'] ?? null); + Assert::string($data['delay'] ?? null); + + $status = null; + if (isset($data['status'])) { + Assert::integerish($data['status']); + $status = StatusEnum::from((int) $data['status']); + } + return new self( itemid: $data['itemid'], name: $data['name'], key_: $data['key_'], hostid: $data['hostid'], - type: ItemTypeEnum::from($data['type']), - value_type: ValueTypeEnum::from($data['value_type']), + type: ItemTypeEnum::from((int) $data['type']), + value_type: ValueTypeEnum::from((int) $data['value_type']), delay: $data['delay'], - interfaceid: $data['interfaceid'] ?? null, - preprocessing: $data['preprocessing'] ?? null, - tags: $data['tags'] ?? null, - status: isset($data['status']) ? StatusEnum::from($data['status']) : null, + interfaceid: isset($data['interfaceid']) && is_string($data['interfaceid']) ? $data['interfaceid'] : null, + preprocessing: isset($data['preprocessing']) && is_array($data['preprocessing']) ? $data['preprocessing'] : null, + tags: isset($data['tags']) && is_array($data['tags']) ? $data['tags'] : null, + status: $status, ); } diff --git a/src/Actions/Dto/PushHistoryResponseDto.php b/src/Actions/Dto/PushHistoryResponseDto.php index 33a8ff0..46eef98 100644 --- a/src/Actions/Dto/PushHistoryResponseDto.php +++ b/src/Actions/Dto/PushHistoryResponseDto.php @@ -17,9 +17,16 @@ public function __construct( public static function fromArray(array $data): self { - $historyids = isset($data['historyids']) && is_array($data['historyids']) - ? array_map(static fn (mixed $id): string => (string) $id, $data['historyids']) - : []; + $historyids = []; + if (isset($data['historyids']) && is_array($data['historyids'])) { + foreach ($data['historyids'] as $id) { + if (is_string($id)) { + $historyids[] = $id; + } elseif (is_int($id)) { + $historyids[] = (string) $id; + } + } + } return new self( historyids: $historyids, diff --git a/src/Actions/Dto/TriggerDto.php b/src/Actions/Dto/TriggerDto.php index d5bcc6c..8d4f33f 100644 --- a/src/Actions/Dto/TriggerDto.php +++ b/src/Actions/Dto/TriggerDto.php @@ -5,6 +5,7 @@ namespace BytesCommerce\ZabbixApi\Actions\Dto; use BytesCommerce\ZabbixApi\Enums\StatusEnum; +use Webmozart\Assert\Assert; final readonly class TriggerDto { @@ -23,16 +24,38 @@ public function __construct( public static function fromArray(array $data): self { + Assert::string($data['triggerid'] ?? null); + Assert::string($data['description'] ?? null); + Assert::string($data['expression'] ?? null); + + $priority = null; + if (isset($data['priority'])) { + Assert::integerish($data['priority']); + $priority = (int) $data['priority']; + } + + $status = null; + if (isset($data['status'])) { + Assert::integerish($data['status']); + $status = StatusEnum::from((int) $data['status']); + } + + $type = null; + if (isset($data['type'])) { + Assert::integerish($data['type']); + $type = (int) $data['type']; + } + return new self( triggerid: $data['triggerid'], description: $data['description'], expression: $data['expression'], - priority: $data['priority'] ?? null, - status: isset($data['status']) ? StatusEnum::from($data['status']) : null, - comments: $data['comments'] ?? null, - type: $data['type'] ?? null, - dependencies: $data['dependencies'] ?? null, - tags: $data['tags'] ?? null, + priority: $priority, + status: $status, + comments: isset($data['comments']) && is_string($data['comments']) ? $data['comments'] : null, + type: $type, + dependencies: isset($data['dependencies']) && is_array($data['dependencies']) ? $data['dependencies'] : null, + tags: isset($data['tags']) && is_array($data['tags']) ? $data['tags'] : null, ); } diff --git a/src/Actions/Dto/UpdateDashboardDto.php b/src/Actions/Dto/UpdateDashboardDto.php index 2dd5624..d3d7259 100644 --- a/src/Actions/Dto/UpdateDashboardDto.php +++ b/src/Actions/Dto/UpdateDashboardDto.php @@ -7,7 +7,7 @@ final readonly class UpdateDashboardDto { /** - * @param list, view_mode?: string}>, private?: int, userid?: string, display_period?: int, auto_start?: int}> $dashboards + * @param list> $dashboards */ public function __construct( public array $dashboards, @@ -15,7 +15,7 @@ public function __construct( } /** - * @return list, view_mode?: string}>, private?: int, userid?: string, display_period?: int, auto_start?: int}> + * @return list> */ public function getDashboards(): array { diff --git a/src/Actions/Event.php b/src/Actions/Event.php index ad5e54e..8026a8c 100644 --- a/src/Actions/Event.php +++ b/src/Actions/Event.php @@ -34,6 +34,9 @@ public static function getActionPrefix(): string public const int ACTION_SEVERITY = 8; + /** + * @param array $params + */ public function get(array $params): GetEventResponseDto { if (!isset($params['output'])) { @@ -42,9 +45,12 @@ public function get(array $params): GetEventResponseDto $result = $this->client->call(ZabbixAction::EVENT_GET, $params); - return GetEventResponseDto::fromArray($result); + return GetEventResponseDto::fromArray(is_array($result) ? $result : []); } + /** + * @param array $params + */ public function acknowledge(array $params): mixed { if (!isset($params['eventids']) || !is_array($params['eventids'])) { diff --git a/src/Actions/Graph.php b/src/Actions/Graph.php index 0d1af3b..27014ba 100644 --- a/src/Actions/Graph.php +++ b/src/Actions/Graph.php @@ -44,7 +44,16 @@ public function create(CreateGraphDto $dto): CreateGraphResponseDto $result = $this->client->call(ZabbixAction::GRAPH_CREATE, $graphs); - return new CreateGraphResponseDto($result['graphids']); + $graphids = []; + if (is_array($result) && isset($result['graphids']) && is_array($result['graphids'])) { + foreach ($result['graphids'] as $id) { + if (is_string($id)) { + $graphids[] = $id; + } + } + } + + return new CreateGraphResponseDto($graphids); } public function update(UpdateGraphDto $dto): UpdateGraphResponseDto @@ -53,7 +62,16 @@ public function update(UpdateGraphDto $dto): UpdateGraphResponseDto $result = $this->client->call(ZabbixAction::GRAPH_UPDATE, $graphs); - return new UpdateGraphResponseDto($result['graphids']); + $graphids = []; + if (is_array($result) && isset($result['graphids']) && is_array($result['graphids'])) { + foreach ($result['graphids'] as $id) { + if (is_string($id)) { + $graphids[] = $id; + } + } + } + + return new UpdateGraphResponseDto($graphids); } public function delete(DeleteGraphDto $dto): DeleteGraphResponseDto diff --git a/src/Actions/History.php b/src/Actions/History.php index a823f3d..1569131 100644 --- a/src/Actions/History.php +++ b/src/Actions/History.php @@ -266,12 +266,12 @@ public function push(array $historyData): PushHistoryResponseDto return new PushHistoryResponseDto([], ''); } - $params = array_map( + $items = array_map( static fn (PushHistoryDto $dto) => $dto->toArray(), $historyData, ); - $result = $this->client->call(ZabbixAction::HISTORY_PUSH, $params); + $result = $this->client->call(ZabbixAction::HISTORY_PUSH, $items); return PushHistoryResponseDto::fromArray(is_array($result) ? $result : []); } diff --git a/src/Actions/Host.php b/src/Actions/Host.php index 007f437..de77a3b 100644 --- a/src/Actions/Host.php +++ b/src/Actions/Host.php @@ -16,6 +16,9 @@ public static function getActionPrefix(): string return 'host'; } + /** + * @param array $params + */ public function get(array $params): GetHostResponseDto { if (!isset($params['output'])) { @@ -24,12 +27,18 @@ public function get(array $params): GetHostResponseDto $result = $this->client->call(ZabbixAction::HOST_GET, $params); - return GetHostResponseDto::fromArray($result); + return GetHostResponseDto::fromArray(is_array($result) ? $result : []); } + /** + * @param list> $hosts + */ public function create(array $hosts): mixed { foreach ($hosts as $host) { + if (!is_array($host)) { + throw new ZabbixApiException('Host must be an array', -1); + } if (!isset($host['host']) || !isset($host['groups']) || !is_array($host['groups'])) { throw new ZabbixApiException('Host creation requires host name and groups array', -1); } @@ -38,9 +47,15 @@ public function create(array $hosts): mixed return $this->client->call(ZabbixAction::HOST_CREATE, $hosts); } + /** + * @param list> $hosts + */ public function update(array $hosts): mixed { foreach ($hosts as $host) { + if (!is_array($host)) { + throw new ZabbixApiException('Host must be an array', -1); + } if (!isset($host['hostid'])) { throw new ZabbixApiException('Host update requires hostid', -1); } @@ -49,11 +64,17 @@ public function update(array $hosts): mixed return $this->client->call(ZabbixAction::HOST_UPDATE, $hosts); } + /** + * @param list $hostIds + */ public function delete(array $hostIds): mixed { return $this->client->call(ZabbixAction::HOST_DELETE, $hostIds); } + /** + * @param array $params + */ public function massAdd(array $params): mixed { if (!isset($params['hosts']) || !is_array($params['hosts'])) { @@ -63,6 +84,9 @@ public function massAdd(array $params): mixed return $this->client->call(ZabbixAction::HOST_MASSADD, $params); } + /** + * @param array $params + */ public function massUpdate(array $params): mixed { if (!isset($params['hosts']) || !is_array($params['hosts'])) { @@ -72,6 +96,9 @@ public function massUpdate(array $params): mixed return $this->client->call(ZabbixAction::HOST_MASSUPDATE, $params); } + /** + * @param array $params + */ public function massRemove(array $params): mixed { if (!isset($params['hostids']) || !is_array($params['hostids'])) { diff --git a/src/Actions/HostGroup.php b/src/Actions/HostGroup.php index e01c15f..67ea433 100644 --- a/src/Actions/HostGroup.php +++ b/src/Actions/HostGroup.php @@ -54,7 +54,16 @@ public function create(CreateHostGroupDto $dto): CreateHostGroupResponseDto $result = $this->client->call(ZabbixAction::HOSTGROUP_CREATE, $params); - return new CreateHostGroupResponseDto($result['groupids']); + $groupids = []; + if (is_array($result) && isset($result['groupids']) && is_array($result['groupids'])) { + foreach ($result['groupids'] as $id) { + if (is_string($id)) { + $groupids[] = $id; + } + } + } + + return new CreateHostGroupResponseDto($groupids); } public function update(UpdateHostGroupDto $dto): UpdateHostGroupResponseDto @@ -63,7 +72,16 @@ public function update(UpdateHostGroupDto $dto): UpdateHostGroupResponseDto $result = $this->client->call(ZabbixAction::HOSTGROUP_UPDATE, $params); - return new UpdateHostGroupResponseDto($result['groupids']); + $groupids = []; + if (is_array($result) && isset($result['groupids']) && is_array($result['groupids'])) { + foreach ($result['groupids'] as $id) { + if (is_string($id)) { + $groupids[] = $id; + } + } + } + + return new UpdateHostGroupResponseDto($groupids); } public function delete(DeleteHostGroupDto $dto): DeleteHostGroupResponseDto @@ -115,7 +133,16 @@ public function massAdd(MassAddHostGroupDto $dto): MassAddHostGroupResponseDto $result = $this->client->call(ZabbixAction::HOSTGROUP_MASSADD, $params); - return new MassAddHostGroupResponseDto($result['groupids'] ?? []); + $groupids = []; + if (is_array($result) && isset($result['groupids']) && is_array($result['groupids'])) { + foreach ($result['groupids'] as $id) { + if (is_string($id)) { + $groupids[] = $id; + } + } + } + + return new MassAddHostGroupResponseDto($groupids); } public function massRemove(MassRemoveHostGroupDto $dto): MassRemoveHostGroupResponseDto @@ -124,7 +151,16 @@ public function massRemove(MassRemoveHostGroupDto $dto): MassRemoveHostGroupResp $result = $this->client->call(ZabbixAction::HOSTGROUP_MASSREMOVE, $params); - return new MassRemoveHostGroupResponseDto($result['groupids'] ?? []); + $groupids = []; + if (is_array($result) && isset($result['groupids']) && is_array($result['groupids'])) { + foreach ($result['groupids'] as $id) { + if (is_string($id)) { + $groupids[] = $id; + } + } + } + + return new MassRemoveHostGroupResponseDto($groupids); } public function massUpdate(MassUpdateHostGroupDto $dto): MassUpdateHostGroupResponseDto @@ -133,7 +169,16 @@ public function massUpdate(MassUpdateHostGroupDto $dto): MassUpdateHostGroupResp $result = $this->client->call(ZabbixAction::HOSTGROUP_MASSUPDATE, $params); - return new MassUpdateHostGroupResponseDto($result['groupids'] ?? []); + $groupids = []; + if (is_array($result) && isset($result['groupids']) && is_array($result['groupids'])) { + foreach ($result['groupids'] as $id) { + if (is_string($id)) { + $groupids[] = $id; + } + } + } + + return new MassUpdateHostGroupResponseDto($groupids); } private function mapCreateHostGroup(array $hostGroup): array diff --git a/src/Actions/Item.php b/src/Actions/Item.php index 7ed67fd..cfc666f 100644 --- a/src/Actions/Item.php +++ b/src/Actions/Item.php @@ -30,7 +30,7 @@ public function get(GetItemDto $dto): GetItemResponseDto $result = $this->client->call(ZabbixAction::ITEM_GET, $params); - return GetItemResponseDto::fromArray($result); + return GetItemResponseDto::fromArray(is_array($result) ? $result : []); } public function create(CreateItemDto $dto): mixed diff --git a/src/Actions/Trapper.php b/src/Actions/Trapper.php index 37d1c16..d65f483 100644 --- a/src/Actions/Trapper.php +++ b/src/Actions/Trapper.php @@ -36,11 +36,15 @@ public function __construct( */ public function send(string $host, string $key, mixed $value, ?int $clock = null): array { + $stringValue = is_scalar($value) || $value instanceof \Stringable + ? (string) $value + : json_encode($value, \JSON_THROW_ON_ERROR); + $data = [ [ 'host' => $host, 'key' => $key, - 'value' => (string) $value + 'value' => $stringValue ] ]; @@ -78,10 +82,15 @@ public function sendBatch(array $metrics): array throw new ZabbixApiException('Each metric must have host, key, and value', -1); } + $value = $metric['value']; + $stringValue = is_scalar($value) || $value instanceof \Stringable + ? (string) $value + : json_encode($value, \JSON_THROW_ON_ERROR); + $item = [ 'host' => $metric['host'], 'key' => $metric['key'], - 'value' => (string) $metric['value'] + 'value' => $stringValue ]; if (isset($metric['clock'])) { @@ -99,7 +108,9 @@ public function sendBatch(array $metrics): array $json = json_encode($payload, \JSON_THROW_ON_ERROR); $message = $this->buildMessage($json); - return $this->sendToZabbix($message); + $result = $this->sendToZabbix($message); + + return $result; } private function buildMessage(string $json): string @@ -110,6 +121,18 @@ private function buildMessage(string $json): string return self::ZABBIX_HEADER . $lengthPacked . $json; } + /** + * @return array{ + * response: string, + * info: string, + * processed?: int, + * failed?: int, + * total?: int, + * seconds_spent?: float + * } + * + * @throws ZabbixApiException + */ private function sendToZabbix(string $message): array { $socket = @stream_socket_client( @@ -158,11 +181,46 @@ private function sendToZabbix(string $message): array throw new ZabbixApiException('Invalid JSON response from Zabbix server: ' . $e->getMessage(), -1); } - if (isset($result['info'])) { - $result = array_merge($result, $this->parseInfoString($result['info'])); + if (!is_array($result)) { + throw new ZabbixApiException('Invalid response from Zabbix server: expected array', -1); } - return $result; + if (isset($result['info']) && is_string($result['info'])) { + $parsed = $this->parseInfoString($result['info']); + foreach ($parsed as $key => $value) { + $result[$key] = $value; + } + } + + $response = $result['response'] ?? null; + $info = $result['info'] ?? null; + + if (!is_string($response) || !is_string($info)) { + throw new ZabbixApiException('Invalid response from Zabbix server: missing required fields', -1); + } + + $typedResult = [ + 'response' => $response, + 'info' => $info, + ]; + + if (isset($result['processed']) && is_int($result['processed'])) { + $typedResult['processed'] = $result['processed']; + } + + if (isset($result['failed']) && is_int($result['failed'])) { + $typedResult['failed'] = $result['failed']; + } + + if (isset($result['total']) && is_int($result['total'])) { + $typedResult['total'] = $result['total']; + } + + if (isset($result['seconds_spent']) && (is_float($result['seconds_spent']) || is_int($result['seconds_spent']))) { + $typedResult['seconds_spent'] = (float) $result['seconds_spent']; + } + + return $typedResult; } private function parseInfoString(string $info): array diff --git a/src/Actions/Trigger.php b/src/Actions/Trigger.php index a1ef8f8..fffeb0d 100644 --- a/src/Actions/Trigger.php +++ b/src/Actions/Trigger.php @@ -16,6 +16,9 @@ public static function getActionPrefix(): string return 'trigger'; } + /** + * @param array $params + */ public function get(array $params): GetTriggerResponseDto { if (!isset($params['output'])) { @@ -24,13 +27,20 @@ public function get(array $params): GetTriggerResponseDto $result = $this->client->call(ZabbixAction::TRIGGER_GET, $params); + if (!is_array($result)) { + throw new ZabbixApiException('Invalid response from Zabbix API: expected array', -1); + } + return GetTriggerResponseDto::fromArray($result); } + /** + * @param array> $triggers + */ public function create(array $triggers): mixed { foreach ($triggers as $trigger) { - if (!isset($trigger['description']) || !isset($trigger['expression'])) { + if (!is_array($trigger) || !isset($trigger['description']) || !isset($trigger['expression'])) { throw new ZabbixApiException('Trigger creation requires description and expression', -1); } } @@ -38,10 +48,13 @@ public function create(array $triggers): mixed return $this->client->call(ZabbixAction::TRIGGER_CREATE, $triggers); } + /** + * @param array> $triggers + */ public function update(array $triggers): mixed { foreach ($triggers as $trigger) { - if (!isset($trigger['triggerid'])) { + if (!is_array($trigger) || !isset($trigger['triggerid'])) { throw new ZabbixApiException('Trigger update requires triggerid', -1); } } @@ -49,6 +62,9 @@ public function update(array $triggers): mixed return $this->client->call(ZabbixAction::TRIGGER_UPDATE, $triggers); } + /** + * @param array $triggerIds + */ public function delete(array $triggerIds): mixed { return $this->client->call(ZabbixAction::TRIGGER_DELETE, $triggerIds); diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index eeaa349..cb1e44d 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -37,7 +37,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->info('Authentication TTL in seconds') ->end() ->scalarNode('app_name') - ->defaultValue('%env(APP_NAME)%') + ->defaultNull() ->info('Application name for monitoring') ->end() ->scalarNode('host_group') diff --git a/src/DependencyInjection/ZabbixApiExtension.php b/src/DependencyInjection/ZabbixApiExtension.php index 084c55a..6e6b166 100644 --- a/src/DependencyInjection/ZabbixApiExtension.php +++ b/src/DependencyInjection/ZabbixApiExtension.php @@ -16,17 +16,26 @@ public function load(array $configs, ContainerBuilder $container): void $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - $container->setParameter('zabbix_api.base_uri', $config['base_uri']); - $container->setParameter('zabbix_api.api_token', $config['api_token']); - $container->setParameter('zabbix_api.username', $config['username']); - $container->setParameter('zabbix_api.password', $config['password']); - $container->setParameter('zabbix_api.auth_ttl', $config['auth_ttl']); - $container->setParameter('zabbix_api.app_name', $config['app_name']); - $container->setParameter('zabbix_api.host_group', $config['host_group']); - $container->setParameter('zabbix_api.dashboard_config_path', $config['dashboard_config_path']); - $container->setParameter('zabbix_api.setup_enabled', $config['setup_enabled']); + $container->setParameter('zabbix_api.base_uri', $this->getConfigValue($config, 'base_uri')); + $container->setParameter('zabbix_api.api_token', $this->getConfigValue($config, 'api_token')); + $container->setParameter('zabbix_api.username', $this->getConfigValue($config, 'username')); + $container->setParameter('zabbix_api.password', $this->getConfigValue($config, 'password')); + $container->setParameter('zabbix_api.auth_ttl', $this->getConfigValue($config, 'auth_ttl')); + $container->setParameter('zabbix_api.app_name', $this->getConfigValue($config, 'app_name')); + $container->setParameter('zabbix_api.host_group', $this->getConfigValue($config, 'host_group')); + $container->setParameter('zabbix_api.dashboard_config_path', $this->getConfigValue($config, 'dashboard_config_path')); + $container->setParameter('zabbix_api.setup_enabled', $this->getConfigValue($config, 'setup_enabled')); $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('services.yaml'); } + + private function getConfigValue(array $config, string $key): array|bool|float|int|string|null + { + $value = $config[$key] ?? null; + if ($value === null || is_array($value) || is_bool($value) || is_float($value) || is_int($value) || is_string($value)) { + return $value; + } + return null; + } } diff --git a/src/Enums/CommandTypeEnum.php b/src/Enums/CommandTypeEnum.php index 9d7946a..01aae5a 100644 --- a/src/Enums/CommandTypeEnum.php +++ b/src/Enums/CommandTypeEnum.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace BytesCommerce\ZabbixApi\Zabbix; +namespace BytesCommerce\ZabbixApi\Enums; enum CommandTypeEnum: int { diff --git a/src/Enums/DefaultMsgEnum.php b/src/Enums/DefaultMsgEnum.php index e11194c..d3103e8 100644 --- a/src/Enums/DefaultMsgEnum.php +++ b/src/Enums/DefaultMsgEnum.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace BytesCommerce\ZabbixApi\Zabbix; +namespace BytesCommerce\ZabbixApi\Enums; enum DefaultMsgEnum: int { diff --git a/src/Enums/ExecuteOnEnum.php b/src/Enums/ExecuteOnEnum.php index e9184db..1c8642f 100644 --- a/src/Enums/ExecuteOnEnum.php +++ b/src/Enums/ExecuteOnEnum.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace BytesCommerce\ZabbixApi\Zabbix; +namespace BytesCommerce\ZabbixApi\Enums; enum ExecuteOnEnum: int { diff --git a/src/Enums/InventoryModeEnum.php b/src/Enums/InventoryModeEnum.php index 14ad670..b837d2a 100644 --- a/src/Enums/InventoryModeEnum.php +++ b/src/Enums/InventoryModeEnum.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace BytesCommerce\ZabbixApi\Zabbix; +namespace BytesCommerce\ZabbixApi\Enums; enum InventoryModeEnum: int { diff --git a/src/Enums/OperationTypeEnum.php b/src/Enums/OperationTypeEnum.php index 6db48b4..612a783 100644 --- a/src/Enums/OperationTypeEnum.php +++ b/src/Enums/OperationTypeEnum.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace BytesCommerce\ZabbixApi\Zabbix; +namespace BytesCommerce\ZabbixApi\Enums; enum OperationTypeEnum: int { diff --git a/src/Enums/SeverityEnum.php b/src/Enums/SeverityEnum.php index 78e2ef9..35dca55 100644 --- a/src/Enums/SeverityEnum.php +++ b/src/Enums/SeverityEnum.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace BytesCommerce\ZabbixApi\Zabbix; +namespace BytesCommerce\ZabbixApi\Enums; enum SeverityEnum: int { diff --git a/src/Enums/ValueEnum.php b/src/Enums/ValueEnum.php index dca32aa..38be4f5 100644 --- a/src/Enums/ValueEnum.php +++ b/src/Enums/ValueEnum.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace BytesCommerce\ZabbixApi\Zabbix; +namespace BytesCommerce\ZabbixApi\Enums; enum ValueEnum: int { diff --git a/src/Provisioning/DashboardApi.php b/src/Provisioning/DashboardApi.php index e4c4128..fc5eb12 100644 --- a/src/Provisioning/DashboardApi.php +++ b/src/Provisioning/DashboardApi.php @@ -123,7 +123,10 @@ public function get(DashboardId $id): ZabbixDashboard Assert::isArray($result); Assert::count($result, 1); - return $this->parseDashboard($result[0]); + $dashboardData = $result[0]; + Assert::isArray($dashboardData); + + return $this->parseDashboard($dashboardData); } public function findHostById(string $hostId): ?HostInfo @@ -139,7 +142,12 @@ public function findHostById(string $hostId): ?HostInfo return null; } - return HostInfo::fromArray($result[0]); + $hostData = $result[0]; + if (!\is_array($hostData)) { + return null; + } + + return HostInfo::fromArray($hostData); } public function findHostByName(string $hostName): ?HostInfo @@ -155,7 +163,12 @@ public function findHostByName(string $hostName): ?HostInfo return null; } - return HostInfo::fromArray($result[0]); + $hostData = $result[0]; + if (!\is_array($hostData)) { + return null; + } + + return HostInfo::fromArray($hostData); } private function parseDashboard(array $data): ZabbixDashboard @@ -164,9 +177,15 @@ private function parseDashboard(array $data): ZabbixDashboard $managedKey = $this->extractManagedKey($widgets); $hash = $this->extractHash($widgets); + $dashboardId = $data['dashboardid'] ?? null; + $name = $data['name'] ?? null; + + Assert::string($dashboardId); + Assert::string($name); + return new ZabbixDashboard( - dashboardId: DashboardId::fromString($data['dashboardid']), - name: $data['name'], + dashboardId: DashboardId::fromString($dashboardId), + name: $name, managedKey: $managedKey, hash: $hash, widgets: $widgets, @@ -177,12 +196,22 @@ private function extractWidgetsFromPages(array $dashboardData): array { $widgets = []; - foreach ($dashboardData['pages'] ?? [] as $page) { + $pages = $dashboardData['pages'] ?? []; + if (!\is_array($pages)) { + return $widgets; + } + + foreach ($pages as $page) { if (!\is_array($page)) { continue; } - foreach ($page['widgets'] ?? [] as $widget) { + $pageWidgets = $page['widgets'] ?? []; + if (!\is_array($pageWidgets)) { + continue; + } + + foreach ($pageWidgets as $widget) { if (\is_array($widget)) { $widgets[] = $widget; } @@ -199,15 +228,26 @@ private function extractManagedKey(array $widgets): ManagedKey continue; } - if (($widget['type'] ?? '') === 'text' && str_contains((string) ($widget['name'] ?? ''), 'Managed')) { - foreach ($widget['fields'] ?? [] as $field) { + $type = $widget['type'] ?? ''; + $name = $widget['name'] ?? ''; + + if ($type === 'text' && is_string($name) && str_contains($name, 'Managed')) { + $fields = $widget['fields'] ?? []; + if (!\is_array($fields)) { + continue; + } + + foreach ($fields as $field) { if (!\is_array($field)) { continue; } - if (($field['type'] ?? '0') === '1' && str_contains((string) ($field['value'] ?? ''), 'Key:')) { - preg_match('/Key:\s*(\S+)/', (string) $field['value'], $matches); - if (isset($matches[1])) { + $fieldType = $field['type'] ?? '0'; + $fieldValue = $field['value'] ?? ''; + + if ($fieldType === '1' && is_string($fieldValue) && str_contains($fieldValue, 'Key:')) { + preg_match('/Key:\s*(\S+)/', $fieldValue, $matches); + if (isset($matches[1]) && is_string($matches[1])) { return ManagedKey::fromString($matches[1]); } } @@ -225,15 +265,26 @@ private function extractHash(array $widgets): DefinitionHash continue; } - if (($widget['type'] ?? '') === 'text' && str_contains((string) ($widget['name'] ?? ''), 'Managed')) { - foreach ($widget['fields'] ?? [] as $field) { + $type = $widget['type'] ?? ''; + $name = $widget['name'] ?? ''; + + if ($type === 'text' && is_string($name) && str_contains($name, 'Managed')) { + $fields = $widget['fields'] ?? []; + if (!\is_array($fields)) { + continue; + } + + foreach ($fields as $field) { if (!\is_array($field)) { continue; } - if (($field['type'] ?? '1') === '1' && str_contains((string) ($field['value'] ?? ''), 'Hash:')) { - preg_match('/Hash:\s*(\S+)/', (string) $field['value'], $matches); - if (isset($matches[1])) { + $fieldType = $field['type'] ?? '1'; + $fieldValue = $field['value'] ?? ''; + + if ($fieldType === '1' && is_string($fieldValue) && str_contains($fieldValue, 'Hash:')) { + preg_match('/Hash:\s*(\S+)/', $fieldValue, $matches); + if (isset($matches[1]) && is_string($matches[1])) { return DefinitionHash::fromString($matches[1]); } } diff --git a/src/Provisioning/DashboardProvisioner.php b/src/Provisioning/DashboardProvisioner.php index 32d2d37..606cdaf 100644 --- a/src/Provisioning/DashboardProvisioner.php +++ b/src/Provisioning/DashboardProvisioner.php @@ -52,8 +52,19 @@ public function provisionForHost( $hash = $this->specHasher->hash($definition); - $title = $this->specRenderer->renderTitleTemplate($definition['title_template'], $host); - $widgets = $this->specRenderer->renderWidgets($definition['widgets'], $host); + $titleTemplate = $definition['title_template'] ?? ''; + $widgetsDef = $definition['widgets'] ?? []; + + if (!is_string($titleTemplate)) { + throw new RuntimeException('Invalid definition: title_template must be a string'); + } + + if (!is_array($widgetsDef)) { + throw new RuntimeException('Invalid definition: widgets must be an array'); + } + + $title = $this->specRenderer->renderTitleTemplate($titleTemplate, $host); + $widgets = $this->specRenderer->renderWidgets($widgetsDef, $host); $spec = new DashboardSpec( name: $title, diff --git a/src/Provisioning/Dto/DashboardSpec.php b/src/Provisioning/Dto/DashboardSpec.php index df345b5..a5ee580 100644 --- a/src/Provisioning/Dto/DashboardSpec.php +++ b/src/Provisioning/Dto/DashboardSpec.php @@ -6,6 +6,7 @@ use BytesCommerce\ZabbixApi\Provisioning\ValueObject\DefinitionHash; use BytesCommerce\ZabbixApi\Provisioning\ValueObject\ManagedKey; +use Webmozart\Assert\Assert; final readonly class DashboardSpec { @@ -19,6 +20,11 @@ public function __construct( public static function fromArray(array $data): self { + Assert::string($data['name'] ?? null); + Assert::string($data['managed_key'] ?? null); + Assert::string($data['hash'] ?? null); + Assert::isArray($data['widgets'] ?? null); + return new self( name: $data['name'], managedKey: ManagedKey::fromString($data['managed_key']), diff --git a/src/Provisioning/Dto/HostInfo.php b/src/Provisioning/Dto/HostInfo.php index f7389c4..3c486a9 100644 --- a/src/Provisioning/Dto/HostInfo.php +++ b/src/Provisioning/Dto/HostInfo.php @@ -4,6 +4,8 @@ namespace BytesCommerce\ZabbixApi\Provisioning\Dto; +use Webmozart\Assert\Assert; + final readonly class HostInfo { public function __construct( @@ -15,6 +17,10 @@ public function __construct( public static function fromArray(array $data): self { + Assert::string($data['hostid'] ?? null); + Assert::string($data['host'] ?? null); + Assert::string($data['name'] ?? null); + return new self( hostId: $data['hostid'], host: $data['host'], diff --git a/src/Provisioning/SpecRenderer.php b/src/Provisioning/SpecRenderer.php index b6a7a47..794027b 100644 --- a/src/Provisioning/SpecRenderer.php +++ b/src/Provisioning/SpecRenderer.php @@ -36,13 +36,30 @@ private function renderArray(array $data, array $replacements): array { $result = []; + $search = array_keys($replacements); + $replace = array_values($replacements); + + $searchStrings = []; + foreach ($search as $s) { + if (is_string($s)) { + $searchStrings[] = $s; + } + } + + $replaceStrings = []; + foreach ($replace as $r) { + if (is_string($r)) { + $replaceStrings[] = $r; + } + } + foreach ($data as $key => $value) { if (\is_array($value)) { $result[$key] = $this->renderArray($value, $replacements); } elseif (\is_string($value)) { $result[$key] = str_replace( - array_keys($replacements), - array_values($replacements), + $searchStrings, + $replaceStrings, $value, ); } else { diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index a7661df..04b3e65 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -21,6 +21,7 @@ services: arguments: - '@http_client' - '%zabbix_api.base_uri%' + autoconfigure: false BytesCommerce\ZabbixApi\ZabbixClientInterface: class: BytesCommerce\ZabbixApi\ZabbixClient diff --git a/src/Service/ZabbixClientWithApiKey.php b/src/Service/ZabbixClientWithApiKey.php index 504368e..c9eddb5 100644 --- a/src/Service/ZabbixClientWithApiKey.php +++ b/src/Service/ZabbixClientWithApiKey.php @@ -164,7 +164,14 @@ private function executeApiCall(ZabbixAction $action, array $params, ?string $au $data = $response->toArray(); if (isset($data['error']) && \is_array($data['error'])) { - $error = ResponseValidator::ensureErrorStructure($data['error']); + $errorData = $data['error']; + $typedError = []; + foreach ($errorData as $key => $value) { + if (is_string($key)) { + $typedError[$key] = $value; + } + } + $error = ResponseValidator::ensureErrorStructure($typedError); throw new ZabbixApiException( $error['message'], diff --git a/src/Service/ZabbixItemRegistry.php b/src/Service/ZabbixItemRegistry.php index db6b12f..87e82b6 100644 --- a/src/Service/ZabbixItemRegistry.php +++ b/src/Service/ZabbixItemRegistry.php @@ -25,7 +25,11 @@ public function getHostId(): ?string $item = $this->cache->getItem(self::CACHE_KEY_HOST_ID); $hostId = $item->get(); - return $hostId !== null ? (string) $hostId : null; + if ($hostId === null) { + return null; + } + + return is_string($hostId) ? $hostId : null; } public function setHostId(string $hostId): void @@ -43,7 +47,9 @@ public function getItemIdForKey(string $key): ?string return null; } - return $itemIds[$key] ?? null; + $value = $itemIds[$key] ?? null; + + return is_string($value) ? $value : null; } public function setItemId(string $key, string $itemId): void @@ -62,10 +68,11 @@ public function getAllItemDefinitions(): array { return [ 'tx.duration_ms' => [ - 'name' => 'Transaction Duration (ms)', + 'name' => 'Transaction Duration', 'type' => 2, 'value_type' => 0, 'history' => '7d', + 'units' => 'ms', ], 'tx.http_status' => [ 'name' => 'HTTP Status Code', @@ -73,6 +80,13 @@ public function getAllItemDefinitions(): array 'value_type' => 3, 'history' => '7d', ], + 'tx.error_rate' => [ + 'name' => 'HTTP Error Rate', + 'type' => 2, + 'value_type' => 0, + 'history' => '7d', + 'units' => '%', + ], 'auth.login.success' => [ 'name' => 'Login Success Count', 'type' => 2, @@ -121,6 +135,51 @@ public function getAllItemDefinitions(): array 'value_type' => 4, 'history' => '7d', ], + 'messenger.queue.depth' => [ + 'name' => 'Message Queue Depth', + 'type' => 2, + 'value_type' => 3, + 'history' => '7d', + ], + 'messenger.failed.count' => [ + 'name' => 'Failed Messages Count', + 'type' => 2, + 'value_type' => 3, + 'history' => '7d', + ], + 'messenger.processing_ms' => [ + 'name' => 'Message Processing Time', + 'type' => 2, + 'value_type' => 0, + 'history' => '7d', + 'units' => 'ms', + ], + 'messenger.received' => [ + 'name' => 'Messages Received', + 'type' => 2, + 'value_type' => 3, + 'history' => '7d', + ], + 'messenger.handled' => [ + 'name' => 'Messages Handled', + 'type' => 2, + 'value_type' => 3, + 'history' => '7d', + ], + 'cache.hit_rate' => [ + 'name' => 'Cache Hit Rate', + 'type' => 2, + 'value_type' => 0, + 'history' => '7d', + 'units' => '%', + ], + 'db.query_time_ms' => [ + 'name' => 'Database Query Time', + 'type' => 2, + 'value_type' => 0, + 'history' => '7d', + 'units' => 'ms', + ], ]; } diff --git a/src/Setup/TriggerProvisioner.php b/src/Setup/TriggerProvisioner.php new file mode 100644 index 0000000..60fb40d --- /dev/null +++ b/src/Setup/TriggerProvisioner.php @@ -0,0 +1,150 @@ +getTriggerDefinitions($hostId, $hostName); + + foreach ($triggerDefinitions as $triggerDef) { + if (is_array($triggerDef)) { + $this->ensureTrigger($triggerDef, $hostId); + } + } + } + + /** + * @return array> + */ + private function getTriggerDefinitions(string $hostId, string $hostName): array + { + $durationKey = $this->naming->getItemKey('tx.duration_ms'); + $statusKey = $this->naming->getItemKey('tx.http_status'); + $loginFailureKey = $this->naming->getItemKey('auth.login.failure'); + $exceptionKey = $this->naming->getItemKey('error.exception'); + $messengerFailedKey = $this->naming->getItemKey('messenger.failed.count'); + + return [ + [ + 'description' => 'High response time on ' . $hostName, + 'expression' => sprintf('last(/%s/%s) > 2000', $hostName, $durationKey), + 'priority' => self::PRIORITY_WARNING, + 'tags' => [ + ['tag' => 'slo', 'value' => 'latency'], + ['tag' => 'class', 'value' => 'performance'], + ], + ], + [ + 'description' => 'Critical response time on ' . $hostName, + 'expression' => sprintf('last(/%s/%s) > 5000', $hostName, $durationKey), + 'priority' => self::PRIORITY_HIGH, + 'tags' => [ + ['tag' => 'slo', 'value' => 'latency'], + ['tag' => 'class', 'value' => 'performance'], + ], + ], + [ + 'description' => 'HTTP 5xx error detected on ' . $hostName, + 'expression' => sprintf('last(/%s/%s) >= 500', $hostName, $statusKey), + 'priority' => self::PRIORITY_HIGH, + 'tags' => [ + ['tag' => 'slo', 'value' => 'availability'], + ['tag' => 'class', 'value' => 'error'], + ], + ], + [ + 'description' => 'HTTP 4xx error detected on ' . $hostName, + 'expression' => sprintf('last(/%s/%s) >= 400 and last(/%s/%s) < 500', $hostName, $statusKey, $hostName, $statusKey), + 'priority' => self::PRIORITY_AVERAGE, + 'tags' => [ + ['tag' => 'class', 'value' => 'client-error'], + ], + ], + [ + 'description' => 'Login failure spike detected on ' . $hostName, + 'expression' => sprintf('avg(/%s/%s,5m) > 10', $hostName, $loginFailureKey), + 'priority' => self::PRIORITY_HIGH, + 'tags' => [ + ['tag' => 'security', 'value' => 'brute-force'], + ['tag' => 'class', 'value' => 'security'], + ], + ], + [ + 'description' => 'Exception rate high on ' . $hostName, + 'expression' => sprintf('count(/%s/%s,5m) > 5', $hostName, $exceptionKey), + 'priority' => self::PRIORITY_WARNING, + 'tags' => [ + ['tag' => 'class', 'value' => 'error'], + ], + ], + [ + 'description' => 'Failed messages in queue on ' . $hostName, + 'expression' => sprintf('last(/%s/%s) > 0', $hostName, $messengerFailedKey), + 'priority' => self::PRIORITY_WARNING, + 'tags' => [ + ['tag' => 'class', 'value' => 'messenger'], + ], + ], + ]; + } + + /** + * @param array $triggerDef + */ + private function ensureTrigger(array $triggerDef, string $hostId): void + { + $existingTriggers = $this->triggerAction->get([ + 'hostids' => [$hostId], + 'filter' => ['description' => $triggerDef['description']], + 'output' => ['triggerid'], + ]); + + if (count($existingTriggers->triggers) > 0) { + $this->logger->debug('Trigger already exists', ['description' => $triggerDef['description']]); + + return; + } + + try { + $result = $this->triggerAction->create([$triggerDef]); + + if (is_array($result)) { + Assert::keyExists($result, 'triggerids', 'Failed to create trigger'); + + $triggerIds = $result['triggerids']; + $triggerId = is_array($triggerIds) && isset($triggerIds[0]) ? $triggerIds[0] : null; + + $this->logger->info('Trigger created', [ + 'description' => $triggerDef['description'], + 'triggerid' => $triggerId, + ]); + } + } catch (\Throwable $e) { + $this->logger->error('Failed to create trigger', [ + 'description' => $triggerDef['description'], + 'error' => $e->getMessage(), + ]); + } + } +} diff --git a/src/Setup/ZabbixSetup.php b/src/Setup/ZabbixSetup.php index 48e326c..eae9e13 100644 --- a/src/Setup/ZabbixSetup.php +++ b/src/Setup/ZabbixSetup.php @@ -23,6 +23,7 @@ public function __construct( private LoggerInterface $logger, #[Autowire('%zabbix_api.setup_enabled%')] private bool $setupEnabled, + private ?TriggerProvisioner $triggerProvisioner = null, ) { } @@ -50,6 +51,8 @@ public function ensureAll(): void $this->registry->setHostId($hostId); $this->ensureItems($hostId); + + $this->ensureTriggers($hostId); } public function ensureHost(): string @@ -62,8 +65,13 @@ public function ensureHost(): string 'output' => ['hostid'], ]); - if (\count($result) > 0 && !empty($result[0]['hostid'])) { - return $result[0]['hostid']; + Assert::isArray($result); + if (count($result) > 0) { + Assert::isArray($result[0]); + $hostId = $result[0]['hostid'] ?? null; + if (is_string($hostId) && $hostId !== '') { + return $hostId; + } } $groupId = $this->ensureHostGroup(); @@ -89,7 +97,11 @@ public function ensureHost(): string ], ]); + Assert::isArray($result); + Assert::keyExists($result, 'hostids'); + Assert::isArray($result['hostids']); $hostId = $result['hostids'][0]; + Assert::string($hostId); $this->logger->info('Zabbix host created', ['host' => $hostName, 'hostid' => $hostId]); return $hostId; @@ -104,16 +116,24 @@ private function ensureHostGroup(): string 'output' => ['groupid'], ]); - if (\count($result) > 0 && !empty($result[0]['groupid'])) { - return $result[0]['groupid']; + Assert::isArray($result); + if (count($result) > 0) { + Assert::isArray($result[0]); + $groupId = $result[0]['groupid'] ?? null; + if (is_string($groupId) && $groupId !== '') { + return $groupId; + } } $result = $this->client->call(ZabbixAction::HOSTGROUP_CREATE, [ 'name' => $hostGroup, ]); + Assert::isArray($result); Assert::keyExists($result, 'groupids', 'Failed to create Zabbix host group'); + Assert::isArray($result['groupids']); $groupId = $result['groupids'][0]; + Assert::string($groupId); $this->logger->info('Zabbix host group created', ['group' => $hostGroup, 'groupid' => $groupId]); return $groupId; @@ -124,6 +144,7 @@ private function ensureItems(string $hostId): void $itemDefinitions = $this->registry->getAllItemDefinitions(); foreach ($itemDefinitions as $suffix => $definition) { + Assert::isArray($definition); $key = $this->registry->getFullItemKey($suffix); $result = $this->client->call(ZabbixAction::ITEM_GET, [ @@ -132,22 +153,36 @@ private function ensureItems(string $hostId): void 'output' => ['itemid'], ]); - if (\count($result) > 0 && !empty($result[0]['itemid'])) { - $itemId = $result[0]['itemid']; - $this->registry->setItemId($key, $itemId); - - continue; + Assert::isArray($result); + if (count($result) > 0) { + Assert::isArray($result[0]); + $itemId = $result[0]['itemid'] ?? null; + if (is_string($itemId) && $itemId !== '') { + $this->registry->setItemId($key, $itemId); + continue; + } } try { - $result = $this->client->call(ZabbixAction::ITEM_CREATE, [ + Assert::keyExists($definition, 'name'); + Assert::keyExists($definition, 'type'); + Assert::keyExists($definition, 'value_type'); + Assert::keyExists($definition, 'history'); + + $itemData = [ 'name' => $definition['name'], 'key_' => $key, 'hostid' => $hostId, 'type' => $definition['type'], 'value_type' => $definition['value_type'], 'history' => $definition['history'], - ]); + ]; + + if (isset($definition['units'])) { + $itemData['units'] = $definition['units']; + } + + $result = $this->client->call(ZabbixAction::ITEM_CREATE, $itemData); } catch (ZabbixApiException $e) { $this->logger->error('Failed to create Zabbix item', [ 'key' => $key, @@ -158,10 +193,23 @@ private function ensureItems(string $hostId): void continue; } - Assert::keyExists($result, 'itemids', \sprintf('Failed to create Zabbix item, expected key "itemids" in response, got %s', implode(',', array_keys($result)))); + Assert::isArray($result); + Assert::keyExists($result, 'itemids', sprintf('Failed to create Zabbix item, expected key "itemids" in response, got %s', implode(',', array_keys($result)))); + Assert::isArray($result['itemids']); $itemId = $result['itemids'][0]; + Assert::string($itemId); $this->registry->setItemId($key, $itemId); $this->logger->info('Zabbix item created', ['key' => $key, 'itemid' => $itemId]); } } + + private function ensureTriggers(string $hostId): void + { + if ($this->triggerProvisioner === null) { + return; + } + + $hostName = $this->naming->getHostName(); + $this->triggerProvisioner->provisionTriggers($hostId, $hostName); + } } diff --git a/src/Subscriber/DoctrineEntitySubscriber.php b/src/Subscriber/DoctrineEntitySubscriber.php index 67fd168..fb9f8ad 100644 --- a/src/Subscriber/DoctrineEntitySubscriber.php +++ b/src/Subscriber/DoctrineEntitySubscriber.php @@ -41,25 +41,25 @@ public function __construct( $excluded[] = $class; } } - $this->excludedEntityClasses = array_unique($excluded); + $this->excludedEntityClasses = array_values(array_unique($excluded)); } public function postPersist(PostPersistEventArgs $event): void { - $this->dispatch($event->getObject(), 'insert'); + $this->dispatch($event->getObject(), 'entity.persist.success'); } public function postUpdate(PostUpdateEventArgs $event): void { - $this->dispatch($event->getObject(), 'update'); + $this->dispatch($event->getObject(), 'entity.update.success'); } public function postRemove(PostRemoveEventArgs $event): void { - $this->dispatch($event->getObject(), 'delete'); + $this->dispatch($event->getObject(), 'entity.remove.success'); } - private function dispatch(object $entity, string $operation): void + private function dispatch(object $entity, string $metricKey): void { if ($this->isExcluded($entity)) { return; @@ -68,12 +68,11 @@ private function dispatch(object $entity, string $operation): void $entityClass = str_replace('\\', '.', $entity::class); $this->bus->dispatch(new PushMetricMessage( - key: $this->naming->getItemKey('doctrine.entity_change'), + key: $this->naming->getItemKey($metricKey), value: 1, tags: [ 'env' => $this->appEnv, 'entity' => $entityClass, - 'operation' => $operation, ], )); } diff --git a/src/Subscriber/ExceptionSubscriber.php b/src/Subscriber/ExceptionSubscriber.php index eb47a54..dbecd35 100644 --- a/src/Subscriber/ExceptionSubscriber.php +++ b/src/Subscriber/ExceptionSubscriber.php @@ -37,7 +37,7 @@ public function __construct( $excluded[] = $class; } } - $this->excludedExceptionClasses = array_unique($excluded); + $this->excludedExceptionClasses = array_values(array_unique($excluded)); } public static function getSubscribedEvents(): array @@ -50,8 +50,10 @@ public static function getSubscribedEvents(): array public function onException(ExceptionEvent $event): void { $req = $event->getRequest(); - $cid = (string) $req->attributes->get('_mon_cid', ''); - $route = (string) ($req->attributes->get('_route') ?? 'unknown'); + $cidRaw = $req->attributes->get('_mon_cid', ''); + $cid = is_string($cidRaw) ? $cidRaw : ''; + $routeRaw = $req->attributes->get('_route'); + $route = is_string($routeRaw) ? $routeRaw : 'unknown'; $exception = $event->getThrowable(); diff --git a/src/Subscriber/MessengerMonitoringSubscriber.php b/src/Subscriber/MessengerMonitoringSubscriber.php new file mode 100644 index 0000000..88a5763 --- /dev/null +++ b/src/Subscriber/MessengerMonitoringSubscriber.php @@ -0,0 +1,86 @@ + 'onMessageReceived', + WorkerMessageHandledEvent::class => 'onMessageHandled', + WorkerMessageFailedEvent::class => 'onMessageFailed', + ]; + } + + public function onMessageReceived(WorkerMessageReceivedEvent $event): void + { + $messageClass = $this->getShortClassName($event->getEnvelope()->getMessage()::class); + + $this->bus->dispatch(new PushMetricMessage( + key: $this->naming->getItemKey('messenger.received'), + value: 1, + tags: [ + 'env' => $this->appEnv, + 'message_class' => $messageClass, + 'transport' => $event->getReceiverName(), + ], + )); + } + + public function onMessageHandled(WorkerMessageHandledEvent $event): void + { + $messageClass = $this->getShortClassName($event->getEnvelope()->getMessage()::class); + + $this->bus->dispatch(new PushMetricMessage( + key: $this->naming->getItemKey('messenger.handled'), + value: 1, + tags: [ + 'env' => $this->appEnv, + 'message_class' => $messageClass, + ], + )); + } + + public function onMessageFailed(WorkerMessageFailedEvent $event): void + { + $messageClass = $this->getShortClassName($event->getEnvelope()->getMessage()::class); + $errorClass = $this->getShortClassName($event->getThrowable()::class); + + $this->bus->dispatch(new PushMetricMessage( + key: $this->naming->getItemKey('messenger.failed.count'), + value: 1, + tags: [ + 'env' => $this->appEnv, + 'message_class' => $messageClass, + 'error_class' => $errorClass, + ], + )); + } + + private function getShortClassName(string $fqcn): string + { + $parts = explode('\\', $fqcn); + + return end($parts); + } +} diff --git a/src/Subscriber/RequestTransactionSubscriber.php b/src/Subscriber/RequestTransactionSubscriber.php index bd53831..7c144e3 100644 --- a/src/Subscriber/RequestTransactionSubscriber.php +++ b/src/Subscriber/RequestTransactionSubscriber.php @@ -68,10 +68,12 @@ public function onTerminate(TerminateEvent $event): void return; } - $cid = (string) $req->attributes->get('_mon_cid', ''); + $cidRaw = $req->attributes->get('_mon_cid', ''); + $cid = is_string($cidRaw) ? $cidRaw : ''; $durationMs = (hrtime(true) - $start) / 1_000_000; - $route = (string) ($req->attributes->get('_route') ?? 'unknown'); + $routeRaw = $req->attributes->get('_route'); + $route = is_string($routeRaw) ? $routeRaw : 'unknown'; if (isset($this->excludedRoutes[$route])) { return; } diff --git a/src/Subscriber/SecurityAuthSubscriber.php b/src/Subscriber/SecurityAuthSubscriber.php index 1f2c100..6435893 100644 --- a/src/Subscriber/SecurityAuthSubscriber.php +++ b/src/Subscriber/SecurityAuthSubscriber.php @@ -74,7 +74,7 @@ public function onFailure(LoginFailureEvent $event): void private function extractUserId(mixed $user): string { if (!\is_object($user)) { - return (string) $user; + return is_string($user) ? $user : 'unknown'; } if (method_exists($user, 'getId')) { @@ -85,7 +85,8 @@ private function extractUserId(mixed $user): string } if (method_exists($user, 'getUserIdentifier')) { - return (string) $user->getUserIdentifier(); + $identifier = $user->getUserIdentifier(); + return is_string($identifier) ? $identifier : 'unknown'; } return 'unknown'; @@ -95,7 +96,8 @@ private function extractUserIdentifier(AuthenticationException $exception, mixed { if (\is_object($user)) { if (method_exists($user, 'getUserIdentifier')) { - return (string) $user->getUserIdentifier(); + $identifier = $user->getUserIdentifier(); + return is_string($identifier) ? $identifier : 'unknown'; } if (method_exists($user, 'getId')) { diff --git a/src/Support/ResponseValidator.php b/src/Support/ResponseValidator.php index 48c0ba2..42c5dfc 100644 --- a/src/Support/ResponseValidator.php +++ b/src/Support/ResponseValidator.php @@ -26,7 +26,13 @@ public static function ensureArray(mixed $result): array ); } - return $result; + $typed = []; + foreach ($result as $key => $value) { + Assert::string($key, sprintf('Array key must be string, got %s', get_debug_type($key))); + $typed[$key] = $value; + } + + return $typed; } /** diff --git a/src/ZabbixClient.php b/src/ZabbixClient.php index 1e3eb4c..2a90385 100644 --- a/src/ZabbixClient.php +++ b/src/ZabbixClient.php @@ -133,6 +133,9 @@ private function doLogin(): string return $result; } + /** + * @param array $params + */ private function executeApiCall(ZabbixAction $action, array $params, ?string $authToken): mixed { $method = $action->value; @@ -166,7 +169,9 @@ private function executeApiCall(ZabbixAction $action, array $params, ?string $au $data = $response->toArray(); if (isset($data['error']) && is_array($data['error'])) { - $error = ResponseValidator::ensureErrorStructure($data['error']); + $error = ResponseValidator::ensureErrorStructure( + ResponseValidator::ensureArray($data['error']), + ); throw new ZabbixApiException( $error['message'], $error['code'], diff --git a/src/ZabbixClientInterface.php b/src/ZabbixClientInterface.php index 47741a3..3cbdab0 100644 --- a/src/ZabbixClientInterface.php +++ b/src/ZabbixClientInterface.php @@ -8,5 +8,8 @@ interface ZabbixClientInterface { + /** + * @param array $params + */ public function call(ZabbixAction $action, array $params = []): mixed; } diff --git a/tests/ActionTest.php b/tests/ActionTest.php index 55b0d14..9631c99 100644 --- a/tests/ActionTest.php +++ b/tests/ActionTest.php @@ -5,9 +5,16 @@ namespace BytesCommerce\ZabbixApi\Tests; use BytesCommerce\ZabbixApi\Actions\Action; +use BytesCommerce\ZabbixApi\Actions\Dto\CreateActionDto; +use BytesCommerce\ZabbixApi\Actions\Dto\CreateSingleActionDto; +use BytesCommerce\ZabbixApi\Actions\Dto\DeleteActionDto; +use BytesCommerce\ZabbixApi\Actions\Dto\GetActionDto; use BytesCommerce\ZabbixApi\Actions\Dto\GetActionResponseDto; +use BytesCommerce\ZabbixApi\Actions\Dto\UpdateActionDto; +use BytesCommerce\ZabbixApi\Actions\Dto\UpdateSingleActionDto; +use BytesCommerce\ZabbixApi\Enums\EventSourceEnum; +use BytesCommerce\ZabbixApi\Enums\StatusEnum; use BytesCommerce\ZabbixApi\Enums\ZabbixAction; -use BytesCommerce\ZabbixApi\ZabbixApiException; use BytesCommerce\ZabbixApi\ZabbixClientInterface; use PHPUnit\Framework\TestCase; @@ -25,7 +32,7 @@ protected function setUp(): void public function testGetWithDefaultOutput(): void { - $params = ['filter' => ['eventsource' => 0]]; + $dto = new GetActionDto(filter: ['eventsource' => 0]); $expectedParams = ['filter' => ['eventsource' => 0], 'output' => 'extend']; $apiResult = [['actionid' => '1', 'name' => 'Test Action', 'eventsource' => 0, 'esc_period' => '1h']]; @@ -34,7 +41,7 @@ public function testGetWithDefaultOutput(): void ->with(ZabbixAction::ACTION_GET, $expectedParams) ->willReturn($apiResult); - $result = $this->action->get($params); + $result = $this->action->get($dto); self::assertInstanceOf(GetActionResponseDto::class, $result); self::assertCount(1, $result->actions); @@ -44,15 +51,16 @@ public function testGetWithDefaultOutput(): void public function testGetWithCustomOutput(): void { - $params = ['output' => ['actionid', 'name'], 'filter' => ['eventsource' => 0]]; + $dto = new GetActionDto(output: 'extend', filter: ['eventsource' => 0]); + $expectedParams = ['output' => 'extend', 'filter' => ['eventsource' => 0]]; $apiResult = [['actionid' => '1', 'name' => 'Test Action', 'eventsource' => 0, 'esc_period' => '1h']]; $this->zabbixClient->expects(self::once()) ->method('call') - ->with(ZabbixAction::ACTION_GET, $params) + ->with(ZabbixAction::ACTION_GET, $expectedParams) ->willReturn($apiResult); - $result = $this->action->get($params); + $result = $this->action->get($dto); self::assertInstanceOf(GetActionResponseDto::class, $result); self::assertCount(1, $result->actions); @@ -62,88 +70,56 @@ public function testGetWithCustomOutput(): void public function testCreateValid(): void { - $actions = [ - [ - 'name' => 'Auto-notify Admin', - 'eventsource' => 0, - 'esc_period' => '1h', - 'operations' => [['operationtype' => 0, 'opmessage' => ['default_msg' => 1], 'opmessage_grp' => [['usrgrpid' => '7']]]] - ] - ]; + $singleAction = new CreateSingleActionDto( + name: 'Auto-notify Admin', + eventsource: EventSourceEnum::TRIGGER, + esc_period: '1h', + operations: [['operationtype' => 0, 'opmessage' => ['default_msg' => 1], 'opmessage_grp' => [['usrgrpid' => '7']]]] + ); + $dto = new CreateActionDto([$singleAction]); + $expectedResult = ['actionids' => ['15']]; $this->zabbixClient->expects(self::once()) ->method('call') - ->with(ZabbixAction::ACTION_CREATE, $actions) + ->with(ZabbixAction::ACTION_CREATE, self::anything()) ->willReturn($expectedResult); - $result = $this->action->create($actions); - - self::assertSame($expectedResult, $result); - } + $result = $this->action->create($dto); - public function testCreateInvalidMissingName(): void - { - $actions = [ - [ - 'eventsource' => 0, - 'esc_period' => '1h', - 'operations' => [] - ] - ]; - - $this->expectException(ZabbixApiException::class); - $this->expectExceptionMessage('Action creation requires name, eventsource, esc_period, and operations'); - - $this->action->create($actions); + self::assertSame(['15'], $result->actionids); } public function testUpdateValid(): void { - $actions = [ - [ - 'actionid' => '15', - 'status' => 1 - ] - ]; + $singleAction = new UpdateSingleActionDto( + actionid: '15', + status: StatusEnum::DISABLED + ); + $dto = new UpdateActionDto([$singleAction]); + $expectedResult = ['actionids' => ['15']]; $this->zabbixClient->expects(self::once()) ->method('call') - ->with(ZabbixAction::ACTION_UPDATE, $actions) + ->with(ZabbixAction::ACTION_UPDATE, self::anything()) ->willReturn($expectedResult); - $result = $this->action->update($actions); + $result = $this->action->update($dto); - self::assertSame($expectedResult, $result); - } - - public function testUpdateInvalidMissingActionId(): void - { - $actions = [ - [ - 'status' => 1 - ] - ]; - - $this->expectException(ZabbixApiException::class); - $this->expectExceptionMessage('Action update requires actionid'); - - $this->action->update($actions); + self::assertSame(['15'], $result->actionids); } public function testDelete(): void { - $actionIds = ['17', '18']; - $expectedResult = ['actionids' => ['17', '18']]; + $dto = new DeleteActionDto(['17', '18']); $this->zabbixClient->expects(self::once()) ->method('call') - ->with(ZabbixAction::ACTION_DELETE, $actionIds) - ->willReturn($expectedResult); + ->with(ZabbixAction::ACTION_DELETE, ['17', '18']); - $result = $this->action->delete($actionIds); + $this->action->delete($dto); - self::assertSame($expectedResult, $result); + self::assertTrue(true); } } diff --git a/tests/AlertTest.php b/tests/AlertTest.php index a010605..c0044a4 100644 --- a/tests/AlertTest.php +++ b/tests/AlertTest.php @@ -27,7 +27,21 @@ public function testGetWithDefaultOutput(): void { $params = ['userids' => '5']; $expectedParams = ['userids' => '5', 'output' => 'extend']; - $expectedResult = [['alertid' => '1', 'subject' => 'Test Alert']]; + $expectedResult = [[ + 'alertid' => '1', + 'actionid' => '10', + 'eventid' => '100', + 'userid' => '5', + 'clock' => 1672531200, + 'mediatypeid' => 1, + 'sendto' => 'admin@example.com', + 'subject' => 'Test Alert', + 'message' => 'Test Message', + 'status' => 0, + 'retries' => 0, + 'error' => '', + 'esc_step' => 1, + ]]; $this->zabbixClient->expects(self::once()) ->method('call') @@ -42,7 +56,21 @@ public function testGetWithDefaultOutput(): void public function testGetWithCustomOutput(): void { $params = ['output' => ['alertid', 'subject', 'message'], 'status' => 1]; - $expectedResult = [['alertid' => '1', 'subject' => 'Test Alert', 'message' => 'Test Message']]; + $expectedResult = [[ + 'alertid' => '1', + 'actionid' => '10', + 'eventid' => '100', + 'userid' => '5', + 'clock' => 1672531200, + 'mediatypeid' => 1, + 'sendto' => 'admin@example.com', + 'subject' => 'Test Alert', + 'message' => 'Test Message', + 'status' => 1, + 'retries' => 0, + 'error' => '', + 'esc_step' => 1, + ]]; $this->zabbixClient->expects(self::once()) ->method('call') @@ -60,7 +88,21 @@ public function testGetWithDateTimeConversion(): void $timeTill = new DateTime('2023-01-02 00:00:00'); $params = ['time_from' => $timeFrom, 'time_till' => $timeTill]; $expectedParams = ['time_from' => $timeFrom->getTimestamp(), 'time_till' => $timeTill->getTimestamp(), 'output' => 'extend']; - $expectedResult = [['alertid' => '1']]; + $expectedResult = [[ + 'alertid' => '1', + 'actionid' => '10', + 'eventid' => '100', + 'userid' => '5', + 'clock' => 1672531200, + 'mediatypeid' => 1, + 'sendto' => 'admin@example.com', + 'subject' => 'Test', + 'message' => 'Test', + 'status' => 0, + 'retries' => 0, + 'error' => '', + 'esc_step' => 1, + ]]; $this->zabbixClient->expects(self::once()) ->method('call') @@ -76,7 +118,21 @@ public function testGetWithUnixTimestamps(): void { $params = ['time_from' => 1672531200, 'time_till' => 1672617600]; $expectedParams = ['time_from' => 1672531200, 'time_till' => 1672617600, 'output' => 'extend']; - $expectedResult = [['alertid' => '1']]; + $expectedResult = [[ + 'alertid' => '1', + 'actionid' => '10', + 'eventid' => '100', + 'userid' => '5', + 'clock' => 1672531200, + 'mediatypeid' => 1, + 'sendto' => 'admin@example.com', + 'subject' => 'Test', + 'message' => 'Test', + 'status' => 0, + 'retries' => 0, + 'error' => '', + 'esc_step' => 1, + ]]; $this->zabbixClient->expects(self::once()) ->method('call') diff --git a/tests/AuditLogTest.php b/tests/AuditLogTest.php index 3712c88..80cba39 100644 --- a/tests/AuditLogTest.php +++ b/tests/AuditLogTest.php @@ -31,7 +31,15 @@ public function testGetWithDefaultOutputAndSelectDetails(): void 'output' => 'extend', 'selectDetails' => 'extend', ]; - $expectedResult = [['auditid' => '1', 'clock' => '1672531200']]; + $expectedResult = [[ + 'auditid' => '1', + 'userid' => '1', + 'clock' => 1672531200, + 'action' => '1', + 'resourcetype' => '4', + 'resourceid' => '100', + 'resourcename' => 'Test Host', + ]]; $this->zabbixClient->expects(self::once()) ->method('call') @@ -50,7 +58,15 @@ public function testGetWithCustomOutputAndSelectDetails(): void 'selectDetails' => ['field', 'oldvalue', 'newvalue'], 'filter' => ['resourcetype' => 4, 'action' => 1], ]; - $expectedResult = [['auditid' => '1', 'clock' => '1672531200', 'resourcename' => 'Test Host']]; + $expectedResult = [[ + 'auditid' => '1', + 'userid' => '1', + 'clock' => 1672531200, + 'action' => '1', + 'resourcetype' => '4', + 'resourceid' => '100', + 'resourcename' => 'Test Host', + ]]; $this->zabbixClient->expects(self::once()) ->method('call') @@ -73,7 +89,15 @@ public function testGetWithDateTimeConversion(): void 'output' => 'extend', 'selectDetails' => 'extend', ]; - $expectedResult = [['auditid' => '1']]; + $expectedResult = [[ + 'auditid' => '1', + 'userid' => '1', + 'clock' => 1672531200, + 'action' => '1', + 'resourcetype' => '4', + 'resourceid' => '100', + 'resourcename' => 'Test Host', + ]]; $this->zabbixClient->expects(self::once()) ->method('call') @@ -95,7 +119,15 @@ public function testGetWithUnixTimestamps(): void 'output' => 'extend', 'selectDetails' => 'extend', ]; - $expectedResult = [['auditid' => '1']]; + $expectedResult = [[ + 'auditid' => '1', + 'userid' => '1', + 'clock' => 1672531200, + 'action' => '1', + 'resourcetype' => '4', + 'resourceid' => '100', + 'resourcename' => 'Test Host', + ]]; $this->zabbixClient->expects(self::once()) ->method('call') @@ -116,7 +148,15 @@ public function testGetWithResourceTypeAndAction(): void 'output' => 'extend', 'selectDetails' => 'extend', ]; - $expectedResult = [['auditid' => '1', 'resourcename' => 'Updated Host']]; + $expectedResult = [[ + 'auditid' => '1', + 'userid' => '1', + 'clock' => 1672531200, + 'action' => '1', + 'resourcetype' => '4', + 'resourceid' => '100', + 'resourcename' => 'Updated Host', + ]]; $this->zabbixClient->expects(self::once()) ->method('call') diff --git a/tests/EventTest.php b/tests/EventTest.php index b5e2a90..ada87d3 100644 --- a/tests/EventTest.php +++ b/tests/EventTest.php @@ -27,7 +27,16 @@ public function testGetWithDefaultOutput(): void { $params = ['source' => Event::SOURCE_TRIGGER, 'value' => 1]; $expectedParams = ['source' => Event::SOURCE_TRIGGER, 'value' => 1, 'output' => 'extend']; - $expectedResult = [['eventid' => '1', 'clock' => '1672531200']]; + $expectedResult = [[ + 'eventid' => '1', + 'source' => 0, + 'object' => 0, + 'objectid' => 100, + 'clock' => 1672531200, + 'value' => 1, + 'acknowledged' => 0, + 'ns' => 0, + ]]; $this->zabbixClient->expects(self::once()) ->method('call') @@ -46,7 +55,16 @@ public function testGetWithCustomOutput(): void 'selectHosts' => ['hostid', 'name'], 'source' => Event::SOURCE_TRIGGER, ]; - $expectedResult = [['eventid' => '1', 'clock' => '1672531200', 'value' => 1]]; + $expectedResult = [[ + 'eventid' => '1', + 'source' => 0, + 'object' => 0, + 'objectid' => 100, + 'clock' => 1672531200, + 'value' => 1, + 'acknowledged' => 0, + 'ns' => 0, + ]]; $this->zabbixClient->expects(self::once()) ->method('call') @@ -132,20 +150,17 @@ public function testAcknowledgeInvalidActionNotInt(): void public function testConstants(): void { - // Test source constants self::assertSame(0, Event::SOURCE_TRIGGER); self::assertSame(1, Event::SOURCE_DISCOVERY); self::assertSame(2, Event::SOURCE_AUTOREGISTRATION); self::assertSame(3, Event::SOURCE_INTERNAL); self::assertSame(4, Event::SOURCE_SERVICE); - // Test action constants self::assertSame(1, Event::ACTION_CLOSE); self::assertSame(2, Event::ACTION_ACKNOWLEDGE); self::assertSame(4, Event::ACTION_MESSAGE); self::assertSame(8, Event::ACTION_SEVERITY); - // Test bitmask combinations $acknowledgeAndMessage = Event::ACTION_ACKNOWLEDGE | Event::ACTION_MESSAGE; self::assertSame(6, $acknowledgeAndMessage); diff --git a/tests/HostTest.php b/tests/HostTest.php index 1e1c7af..fd4fe08 100644 --- a/tests/HostTest.php +++ b/tests/HostTest.php @@ -27,7 +27,11 @@ public function testGetWithDefaultOutput(): void { $params = ['filter' => ['status' => 0]]; $expectedParams = ['filter' => ['status' => 0], 'output' => 'extend']; - $expectedResult = [['hostid' => '1', 'host' => 'Test Host']]; + $expectedResult = [[ + 'hostid' => '1', + 'host' => 'Test Host', + 'status' => 0, + ]]; $this->zabbixClient->expects(self::once()) ->method('call') @@ -46,7 +50,12 @@ public function testGetWithCustomOutput(): void 'selectInterfaces' => 'extend', 'selectGroups' => 'extend', ]; - $expectedResult = [['hostid' => '1', 'host' => 'Test Host', 'name' => 'Test Host Display']]; + $expectedResult = [[ + 'hostid' => '1', + 'host' => 'Test Host', + 'name' => 'Test Host Display', + 'status' => 0, + ]]; $this->zabbixClient->expects(self::once()) ->method('call') diff --git a/tests/Integration/ZabbixApiBundleTest.php b/tests/Integration/ZabbixApiBundleTest.php index d84c68b..3850608 100644 --- a/tests/Integration/ZabbixApiBundleTest.php +++ b/tests/Integration/ZabbixApiBundleTest.php @@ -23,14 +23,13 @@ public function testServicesAreRegistered(): void $container = new ContainerBuilder(new ParameterBag([ 'kernel.debug' => false, 'kernel.environment' => 'test', + 'kernel.project_dir' => '/tmp', + 'env(ZABBIX_API_URL)' => 'https://zabbix.test/api_jsonrpc.php', ])); - // Register required framework services as synthetic $container->setDefinition('http_client', (new Definition(HttpClientInterface::class))->setSynthetic(true)); $container->setDefinition(LoggerInterface::class, (new Definition(LoggerInterface::class))->setSynthetic(true)); $container->setDefinition('cache.app', (new Definition(CacheInterface::class))->setSynthetic(true)); - - // Set autowiring aliases so the container can resolve typed parameters $container->setAlias(HttpClientInterface::class, 'http_client')->setPublic(false); $container->setAlias(CacheInterface::class, 'cache.app')->setPublic(false); @@ -48,7 +47,6 @@ public function testServicesAreRegistered(): void ], ], $container); - // Make services public so we can verify they exist after compilation $container->getDefinition(ZabbixClientInterface::class)->setPublic(true); $container->getDefinition(ZabbixServiceInterface::class)->setPublic(true); $container->getDefinition(ActionServiceInterface::class)->setPublic(true); @@ -65,6 +63,8 @@ public function testParametersAreSet(): void $container = new ContainerBuilder(new ParameterBag([ 'kernel.debug' => false, 'kernel.environment' => 'test', + 'kernel.project_dir' => '/tmp', + 'env(ZABBIX_API_URL)' => 'https://zabbix.test/api_jsonrpc.php', ])); $container->setDefinition('http_client', (new Definition(HttpClientInterface::class))->setSynthetic(true)); @@ -100,6 +100,8 @@ public function testDefaultConfigValues(): void $container = new ContainerBuilder(new ParameterBag([ 'kernel.debug' => false, 'kernel.environment' => 'test', + 'kernel.project_dir' => '/tmp', + 'env(ZABBIX_API_URL)' => 'https://zabbix.test/api_jsonrpc.php', ])); $container->setDefinition('http_client', (new Definition(HttpClientInterface::class))->setSynthetic(true)); @@ -115,7 +117,9 @@ public function testDefaultConfigValues(): void self::assertNotNull($extension); $extension->load([ - ['base_uri' => 'https://zabbix.test/api_jsonrpc.php'], + [ + 'base_uri' => 'https://zabbix.test/api_jsonrpc.php', + ], ], $container); self::assertNull($container->getParameter('zabbix_api.username')); diff --git a/tests/TriggerTest.php b/tests/TriggerTest.php index d9c8e61..3f16b91 100644 --- a/tests/TriggerTest.php +++ b/tests/TriggerTest.php @@ -27,7 +27,11 @@ public function testGetWithDefaultOutput(): void { $params = ['filter' => ['value' => 1]]; $expectedParams = ['filter' => ['value' => 1], 'output' => 'extend']; - $expectedResult = [['triggerid' => '1', 'description' => 'Test Trigger']]; + $expectedResult = [[ + 'triggerid' => '1', + 'description' => 'Test Trigger', + 'expression' => 'last(/host/system.cpu.load)>5', + ]]; $this->zabbixClient->expects(self::once()) ->method('call') @@ -42,7 +46,12 @@ public function testGetWithDefaultOutput(): void public function testGetWithCustomOutput(): void { $params = ['output' => ['triggerid', 'description', 'priority'], 'selectHosts' => ['hostid', 'name']]; - $expectedResult = [['triggerid' => '1', 'description' => 'Test Trigger', 'priority' => 4]]; + $expectedResult = [[ + 'triggerid' => '1', + 'description' => 'Test Trigger', + 'expression' => 'last(/host/system.cpu.load)>5', + 'priority' => 4, + ]]; $this->zabbixClient->expects(self::once()) ->method('call') diff --git a/tests/ZabbixClientTest.php b/tests/ZabbixClientTest.php index a8f5c25..3ae4d5c 100644 --- a/tests/ZabbixClientTest.php +++ b/tests/ZabbixClientTest.php @@ -30,7 +30,10 @@ protected function setUp(): void $this->logger = $this->createMock(LoggerInterface::class); $this->cache = $this->createMock(CacheInterface::class); - // Use API token auth for simple test cases (bypasses cache/login flow) + $this->cache->method('get') + ->with('zabbix_bearer_token', self::anything()) + ->willReturn('test-api-token'); + $this->zabbixClient = new ZabbixClient( username: null, password: null, @@ -72,9 +75,9 @@ public function testCallWithError(): void ->method('toArray') ->willReturn([ 'error' => [ - 'code' => -32602, - 'message' => 'Invalid params', - 'data' => 'Invalid parameter', + 'code' => -32700, + 'message' => 'Parse error', + 'data' => 'Invalid JSON', ], ]); @@ -83,7 +86,7 @@ public function testCallWithError(): void ->willReturn($response); $this->expectException(ZabbixApiException::class); - $this->expectExceptionMessage('Invalid params'); + $this->expectExceptionMessage('Parse error'); $this->zabbixClient->call(ZabbixAction::HOST_GET); } @@ -102,28 +105,26 @@ public function testCallWithHttpError(): void public function testCallWithUsernamePasswordAuth(): void { + $cache = $this->createMock(CacheInterface::class); + $cache->method('get') + ->with('zabbix_bearer_token', self::anything()) + ->willReturnCallback(function (string $key, callable $callback): string { + $item = $this->createMock(ItemInterface::class); + $item->method('expiresAfter'); + + return $callback($item); + }); + $client = new ZabbixClient( username: 'testuser', password: 'testpass', apiToken: null, httpClient: $this->httpClient, logger: $this->logger, - cache: $this->cache, + cache: $cache, authTtl: 3600, ); - // Mock cache->get() to simulate login and return a token - $this->cache->expects(self::once()) - ->method('get') - ->with('zabbix_bearer_token', self::anything()) - ->willReturnCallback(function (string $key, callable $callback): string { - $item = $this->createMock(ItemInterface::class); - $item->expects(self::once())->method('expiresAfter')->with(3600); - - return $callback($item); - }); - - // Expect two HTTP calls: one for login, one for the actual API call $loginResponse = $this->createMock(ResponseInterface::class); $loginResponse->method('toArray')->willReturn(['result' => 'auth-token-123']);