diff --git a/CHANGELOG.md b/CHANGELOG.md index c20517d..9fe0b89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to `ohdear-php-sdk` will be documented in this file +## 4.6.0 - 2026-03-26 + +### What's Changed + +* Chore | Upgrade to Saloon v4 by @Sammyjo20 in https://github.com/ohdearapp/ohdear-php-sdk/pull/70 + +### New Contributors + +* @Sammyjo20 made their first contribution in https://github.com/ohdearapp/ohdear-php-sdk/pull/70 + +**Full Changelog**: https://github.com/ohdearapp/ohdear-php-sdk/compare/4.4.3...4.6.0 + +## Add warning state support - 2026-02-28 + +### What changed + +- Added `CheckResult` backed string enum (`pending`, `succeeded`, `warning`, `failed`, `errored-or-timed-out`) with helper methods (`isUp()`, `isDown()`, `isPending()`, `isWarning()`) +- Added `checkResult()` method to `Check`, `Monitor`, and `CheckSummary` DTOs that returns the typed enum + ## Add missing API endpoints - 2026-02-17 ### What changed diff --git a/README.md b/README.md index e40a01e..7196f70 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![Tests](https://github.com/ohdearapp/ohdear-php-sdk/workflows/run-tests/badge.svg) [![Total Downloads](https://img.shields.io/packagist/dt/ohdearapp/ohdear-php-sdk.svg?style=flat-square)](https://packagist.org/packages/ohdearapp/ohdear-php-sdk) -This package is the official PHP SDK for the [Oh Dear](https://ohdear.app) API, built with [Saloon](https://docs.saloon.dev/) v3. +This package is the official PHP SDK for the [Oh Dear](https://ohdear.app) API, built with [Saloon](https://docs.saloon.dev/) v4. ```php use OhDear\PhpSdk\OhDear; @@ -152,9 +152,51 @@ $checkSummary = $ohDear->checkSummary($monitorId, CheckType::CertificateHealth); echo "Check result: {$checkSummary->result}\n"; echo "Summary: {$checkSummary->summary}\n"; + +// Use the checkResult() method to get the typed enum with helper methods +if ($checkSummary->checkResult()->isUp()) { + echo "Monitor is reachable\n"; +} + +if ($checkSummary->checkResult()->isWarning()) { + echo "Partial connectivity issue detected\n"; +} + +if ($checkSummary->checkResult()->isDown()) { + echo "Monitor is down\n"; +} ``` -You can request a summary for all available cases in the `CheckType` enum.` +You can request a summary for all available cases in the `CheckType` enum. + +#### Check result enum + +The `CheckResult` enum represents the possible states of a check. Access it via the `checkResult()` method available on `Check`, `Monitor`, and `CheckSummary` DTOs: + +```php +use OhDear\PhpSdk\Enums\CheckResult; + +$checkSummary = $ohDear->checkSummary($monitorId, CheckType::Uptime); + +// The raw string is still available +echo $checkSummary->result; // 'succeeded', 'warning', 'failed', etc. + +// Use checkResult() for the typed enum +$result = $checkSummary->checkResult(); + +// Available cases: +CheckResult::Pending; // 'pending' +CheckResult::Succeeded; // 'succeeded' +CheckResult::Warning; // 'warning' — primary location reports down, secondary confirms reachable +CheckResult::Failed; // 'failed' +CheckResult::ErroredOrTimedOut; // 'errored-or-timed-out' + +// Helper methods: +$result->isUp(); // true for Succeeded and Warning +$result->isDown(); // true for Failed and ErroredOrTimedOut +$result->isPending(); // true for Pending only +$result->isWarning(); // true for Warning only +``` #### Getting certificate health for a monitor diff --git a/composer.json b/composer.json index b56d824..122de07 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "require": { "php": "^8.1", "saloonphp/pagination-plugin": "^2.2", - "saloonphp/saloon": "^3.14" + "saloonphp/saloon": "^4.0" }, "require-dev": { "laravel/pint": "^1.27", diff --git a/src/Dto/Check.php b/src/Dto/Check.php index da090c1..4715c2d 100644 --- a/src/Dto/Check.php +++ b/src/Dto/Check.php @@ -2,6 +2,8 @@ namespace OhDear\PhpSdk\Dto; +use OhDear\PhpSdk\Enums\CheckResult; + class Check { public function __construct( @@ -17,6 +19,15 @@ public function __construct( public ?array $activeSnooze, ) {} + public function checkResult(): ?CheckResult + { + if ($this->latestRunResult === null) { + return null; + } + + return CheckResult::tryFrom($this->latestRunResult); + } + public static function fromResponse(array $data): self { return new self( diff --git a/src/Dto/CheckSummary.php b/src/Dto/CheckSummary.php index 44149b9..914b180 100644 --- a/src/Dto/CheckSummary.php +++ b/src/Dto/CheckSummary.php @@ -2,18 +2,29 @@ namespace OhDear\PhpSdk\Dto; +use OhDear\PhpSdk\Enums\CheckResult; + class CheckSummary { public function __construct( - public string $result, + public ?string $result, public ?string $summary, ) {} + public function checkResult(): ?CheckResult + { + if ($this->result === null) { + return null; + } + + return CheckResult::tryFrom($this->result); + } + public static function fromResponse(array $data): self { return new self( - result: $data['result'], - summary: $data['summary'], + result: $data['result'] ?? null, + summary: $data['summary'] ?? null, ); } } diff --git a/src/Dto/Monitor.php b/src/Dto/Monitor.php index 77fc585..5c04304 100644 --- a/src/Dto/Monitor.php +++ b/src/Dto/Monitor.php @@ -2,6 +2,8 @@ namespace OhDear\PhpSdk\Dto; +use OhDear\PhpSdk\Enums\CheckResult; + class Monitor { public function __construct( @@ -40,6 +42,15 @@ public function __construct( public ?string $updatedAt = null, ) {} + public function checkResult(): ?CheckResult + { + if ($this->summarizedCheckResult === null) { + return null; + } + + return CheckResult::tryFrom($this->summarizedCheckResult); + } + public static function fromResponse(array $data): self { return new self( diff --git a/src/Enums/CheckResult.php b/src/Enums/CheckResult.php new file mode 100644 index 0000000..6377521 --- /dev/null +++ b/src/Enums/CheckResult.php @@ -0,0 +1,38 @@ + true, + default => false, + }; + } + + public function isDown(): bool + { + return match ($this) { + self::Failed, self::ErroredOrTimedOut => true, + default => false, + }; + } + + public function isPending(): bool + { + return $this === self::Pending; + } + + public function isWarning(): bool + { + return $this === self::Warning; + } +} diff --git a/tests/Fixtures/Saloon/check-summary-warning.json b/tests/Fixtures/Saloon/check-summary-warning.json new file mode 100644 index 0000000..63d6ed8 --- /dev/null +++ b/tests/Fixtures/Saloon/check-summary-warning.json @@ -0,0 +1,21 @@ +{ + "statusCode": 200, + "headers": { + "Date": "Sun, 10 Aug 2025 18:46:17 GMT", + "Content-Type": "application\/json", + "Transfer-Encoding": "chunked", + "Connection": "keep-alive", + "Server": "cloudflare", + "Vary": "Accept-Encoding", + "Cache-Control": "no-cache, private", + "X-Ratelimit-Limit": "500", + "X-Ratelimit-Remaining": "498", + "Access-Control-Allow-Origin": "*", + "X-Frame-Options": "SAMEORIGIN", + "X-Xss-Protection": "1; mode=block", + "X-Content-Type-Options": "nosniff", + "Cf-Cache-Status": "BYPASS" + }, + "data": "{\"result\":\"warning\",\"summary\":\"Primary location reports down, secondary confirms reachable.\"}", + "context": [] +} diff --git a/tests/OhDearTests/CheckResultTest.php b/tests/OhDearTests/CheckResultTest.php new file mode 100644 index 0000000..4152fb0 --- /dev/null +++ b/tests/OhDearTests/CheckResultTest.php @@ -0,0 +1,57 @@ +toHaveCount(5); + expect(CheckResult::Pending->value)->toBe('pending'); + expect(CheckResult::Succeeded->value)->toBe('succeeded'); + expect(CheckResult::Warning->value)->toBe('warning'); + expect(CheckResult::Failed->value)->toBe('failed'); + expect(CheckResult::ErroredOrTimedOut->value)->toBe('errored-or-timed-out'); +}); + +it('can be created from string values', function () { + expect(CheckResult::from('succeeded'))->toBe(CheckResult::Succeeded); + expect(CheckResult::from('warning'))->toBe(CheckResult::Warning); + expect(CheckResult::from('failed'))->toBe(CheckResult::Failed); + expect(CheckResult::from('pending'))->toBe(CheckResult::Pending); + expect(CheckResult::from('errored-or-timed-out'))->toBe(CheckResult::ErroredOrTimedOut); +}); + +it('returns null for unknown values with tryFrom', function () { + expect(CheckResult::tryFrom('unknown'))->toBeNull(); + expect(CheckResult::tryFrom(''))->toBeNull(); +}); + +it('correctly identifies up states', function () { + expect(CheckResult::Succeeded->isUp())->toBeTrue(); + expect(CheckResult::Warning->isUp())->toBeTrue(); + expect(CheckResult::Pending->isUp())->toBeFalse(); + expect(CheckResult::Failed->isUp())->toBeFalse(); + expect(CheckResult::ErroredOrTimedOut->isUp())->toBeFalse(); +}); + +it('correctly identifies down states', function () { + expect(CheckResult::Failed->isDown())->toBeTrue(); + expect(CheckResult::ErroredOrTimedOut->isDown())->toBeTrue(); + expect(CheckResult::Succeeded->isDown())->toBeFalse(); + expect(CheckResult::Warning->isDown())->toBeFalse(); + expect(CheckResult::Pending->isDown())->toBeFalse(); +}); + +it('correctly identifies pending state', function () { + expect(CheckResult::Pending->isPending())->toBeTrue(); + expect(CheckResult::Succeeded->isPending())->toBeFalse(); + expect(CheckResult::Warning->isPending())->toBeFalse(); + expect(CheckResult::Failed->isPending())->toBeFalse(); + expect(CheckResult::ErroredOrTimedOut->isPending())->toBeFalse(); +}); + +it('correctly identifies warning state', function () { + expect(CheckResult::Warning->isWarning())->toBeTrue(); + expect(CheckResult::Succeeded->isWarning())->toBeFalse(); + expect(CheckResult::Failed->isWarning())->toBeFalse(); + expect(CheckResult::Pending->isWarning())->toBeFalse(); + expect(CheckResult::ErroredOrTimedOut->isWarning())->toBeFalse(); +}); diff --git a/tests/OhDearTests/ChecksTest.php b/tests/OhDearTests/ChecksTest.php index 1be6f4f..7e2e18c 100644 --- a/tests/OhDearTests/ChecksTest.php +++ b/tests/OhDearTests/ChecksTest.php @@ -1,5 +1,6 @@ id)->toBe(940704); expect($check->enabled)->toBe(true); + expect($check->latestRunResult)->toBe('succeeded'); + expect($check->checkResult())->toBe(CheckResult::Succeeded); }); it('can disable a check', function () { diff --git a/tests/OhDearTests/MonitorsTest.php b/tests/OhDearTests/MonitorsTest.php index 10021c0..8c8467b 100644 --- a/tests/OhDearTests/MonitorsTest.php +++ b/tests/OhDearTests/MonitorsTest.php @@ -1,5 +1,6 @@ ohDear->monitor(82063); expect($monitor->url)->toBe('https://laravel.com'); + expect($monitor->summarizedCheckResult)->toBe('succeeded'); + expect($monitor->checkResult())->toBe(CheckResult::Succeeded); }); it('can create a monitor', function () { @@ -74,6 +77,24 @@ $checkSummary = $this->ohDear->checkSummary(82060, CheckType::CertificateHealth); expect($checkSummary->result)->toBe('succeeded'); + expect($checkSummary->checkResult())->toBe(CheckResult::Succeeded); + expect($checkSummary->checkResult()->isUp())->toBeTrue(); + expect($checkSummary->checkResult()->isDown())->toBeFalse(); + expect($checkSummary->checkResult()->isWarning())->toBeFalse(); +}); + +it('can get a warning check summary for a monitor', function () { + MockClient::global([ + GetCheckSummaryRequest::class => MockResponse::fixture('check-summary-warning'), + ]); + + $checkSummary = $this->ohDear->checkSummary(82060, CheckType::Uptime); + + expect($checkSummary->result)->toBe('warning'); + expect($checkSummary->checkResult())->toBe(CheckResult::Warning); + expect($checkSummary->checkResult()->isUp())->toBeTrue(); + expect($checkSummary->checkResult()->isDown())->toBeFalse(); + expect($checkSummary->checkResult()->isWarning())->toBeTrue(); }); it('can get notification destinations for a monitor', function () {