From b7d12c8093025482d6aad2031b256e4f1d4659ea Mon Sep 17 00:00:00 2001 From: Fred Myerscough Date: Fri, 3 Apr 2026 13:04:14 +0100 Subject: [PATCH 01/11] refactor(exceptions): add base exception class - Add WebUtilityException abstract base class - All package exceptions now extend WebUtilityException - Enables catching all package exceptions in one go - Add tests for exception hierarchy --- src/Exceptions/ContentNotFoundException.php | 4 +- src/Exceptions/EmptyUrlException.php | 4 +- .../FiveHundredResponseException.php | 4 +- .../FourHundredResponseException.php | 4 +- src/Exceptions/InvalidUrlException.php | 4 +- .../MaxRedirectsReachedException.php | 4 +- src/Exceptions/NetworkErrorException.php | 4 +- .../UnsupportedCheckMethodException.php | 4 +- src/Exceptions/WebUtilityException.php | 9 ++++ src/ResponseUtility.php | 3 +- tests/Exceptions/WebUtilityExceptionTest.php | 48 +++++++++++++++++++ 11 files changed, 66 insertions(+), 26 deletions(-) create mode 100644 src/Exceptions/WebUtilityException.php create mode 100644 tests/Exceptions/WebUtilityExceptionTest.php diff --git a/src/Exceptions/ContentNotFoundException.php b/src/Exceptions/ContentNotFoundException.php index eb90113..c05fcd2 100644 --- a/src/Exceptions/ContentNotFoundException.php +++ b/src/Exceptions/ContentNotFoundException.php @@ -2,8 +2,6 @@ namespace Myerscode\Utilities\Web\Exceptions; -use Exception; - -class ContentNotFoundException extends Exception +class ContentNotFoundException extends WebUtilityException { } diff --git a/src/Exceptions/EmptyUrlException.php b/src/Exceptions/EmptyUrlException.php index efaa034..aceaede 100644 --- a/src/Exceptions/EmptyUrlException.php +++ b/src/Exceptions/EmptyUrlException.php @@ -2,8 +2,6 @@ namespace Myerscode\Utilities\Web\Exceptions; -use Exception; - -class EmptyUrlException extends Exception +class EmptyUrlException extends WebUtilityException { } diff --git a/src/Exceptions/FiveHundredResponseException.php b/src/Exceptions/FiveHundredResponseException.php index 4f958f8..bfb3c79 100644 --- a/src/Exceptions/FiveHundredResponseException.php +++ b/src/Exceptions/FiveHundredResponseException.php @@ -2,8 +2,6 @@ namespace Myerscode\Utilities\Web\Exceptions; -use Exception; - -class FiveHundredResponseException extends Exception +class FiveHundredResponseException extends WebUtilityException { } diff --git a/src/Exceptions/FourHundredResponseException.php b/src/Exceptions/FourHundredResponseException.php index d2d3688..0de9b4c 100644 --- a/src/Exceptions/FourHundredResponseException.php +++ b/src/Exceptions/FourHundredResponseException.php @@ -2,8 +2,6 @@ namespace Myerscode\Utilities\Web\Exceptions; -use Exception; - -class FourHundredResponseException extends Exception +class FourHundredResponseException extends WebUtilityException { } diff --git a/src/Exceptions/InvalidUrlException.php b/src/Exceptions/InvalidUrlException.php index fe6f736..7e49e0c 100644 --- a/src/Exceptions/InvalidUrlException.php +++ b/src/Exceptions/InvalidUrlException.php @@ -2,8 +2,6 @@ namespace Myerscode\Utilities\Web\Exceptions; -use Exception; - -class InvalidUrlException extends Exception +class InvalidUrlException extends WebUtilityException { } diff --git a/src/Exceptions/MaxRedirectsReachedException.php b/src/Exceptions/MaxRedirectsReachedException.php index 7ae975e..78a005e 100644 --- a/src/Exceptions/MaxRedirectsReachedException.php +++ b/src/Exceptions/MaxRedirectsReachedException.php @@ -2,8 +2,6 @@ namespace Myerscode\Utilities\Web\Exceptions; -use Exception; - -class MaxRedirectsReachedException extends Exception +class MaxRedirectsReachedException extends WebUtilityException { } diff --git a/src/Exceptions/NetworkErrorException.php b/src/Exceptions/NetworkErrorException.php index f08ddc7..d8d5a20 100644 --- a/src/Exceptions/NetworkErrorException.php +++ b/src/Exceptions/NetworkErrorException.php @@ -2,8 +2,6 @@ namespace Myerscode\Utilities\Web\Exceptions; -use Exception; - -class NetworkErrorException extends Exception +class NetworkErrorException extends WebUtilityException { } diff --git a/src/Exceptions/UnsupportedCheckMethodException.php b/src/Exceptions/UnsupportedCheckMethodException.php index 810da47..4394285 100644 --- a/src/Exceptions/UnsupportedCheckMethodException.php +++ b/src/Exceptions/UnsupportedCheckMethodException.php @@ -2,8 +2,6 @@ namespace Myerscode\Utilities\Web\Exceptions; -use Exception; - -class UnsupportedCheckMethodException extends Exception +class UnsupportedCheckMethodException extends WebUtilityException { } diff --git a/src/Exceptions/WebUtilityException.php b/src/Exceptions/WebUtilityException.php new file mode 100644 index 0000000..002e918 --- /dev/null +++ b/src/Exceptions/WebUtilityException.php @@ -0,0 +1,9 @@ + [new ContentNotFoundException()]; + yield 'EmptyUrlException' => [new EmptyUrlException()]; + yield 'FiveHundredResponseException' => [new FiveHundredResponseException()]; + yield 'FourHundredResponseException' => [new FourHundredResponseException()]; + yield 'InvalidUrlException' => [new InvalidUrlException()]; + yield 'MaxRedirectsReachedException' => [new MaxRedirectsReachedException()]; + yield 'NetworkErrorException' => [new NetworkErrorException()]; + yield 'UnsupportedCheckMethodException' => [new UnsupportedCheckMethodException()]; + } + + #[DataProvider('exceptionProvider')] + public function testAllExceptionsExtendWebUtilityException(WebUtilityException $exception): void + { + $this->assertInstanceOf(WebUtilityException::class, $exception); + $this->assertInstanceOf(RuntimeException::class, $exception); + } + + public function testCanCatchAllPackageExceptions(): void + { + $this->expectException(WebUtilityException::class); + + throw new ContentNotFoundException('test'); + } +} From ccf985954bf4c1a9c350620c340779b02e810d02 Mon Sep 17 00:00:00 2001 From: Fred Myerscough Date: Fri, 3 Apr 2026 13:04:48 +0100 Subject: [PATCH 02/11] fix(uri): anchor scheme detection regex - Anchor regex to start of string with ^ - Prevents false matches on URLs containing schemes - Applied to both UriUtility and ResponseUtility --- src/ResponseUtility.php | 6 ++---- src/UriUtility.php | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/ResponseUtility.php b/src/ResponseUtility.php index 4d48529..1f1f8fc 100644 --- a/src/ResponseUtility.php +++ b/src/ResponseUtility.php @@ -229,10 +229,8 @@ private function setUrl(string|UriUtility $uri): void { $trimmed = trim($uri instanceof UriUtility ? $uri->value() : $uri); - // check if a scheme is present, if not we need to give it one - preg_match_all('#(https:\/\/)|(http:\/\/)#', $trimmed, $matches, PREG_SET_ORDER, 0); - - if ($matches === []) { + // check if a scheme is present at the start, if not we need to give it one + if (!preg_match('#^https?://#i', $trimmed)) { $trimmed = self::DEFAULT_SCHEME . $trimmed; } diff --git a/src/UriUtility.php b/src/UriUtility.php index 558cfaa..9647465 100644 --- a/src/UriUtility.php +++ b/src/UriUtility.php @@ -203,10 +203,8 @@ private function setUrl(string|Http $uri): void { $trimmed = trim($uri); - // check if a scheme is present, if not we need to give it one - preg_match_all('/(https:\/\/)|(http:\/\/)/', $trimmed, $matches, PREG_SET_ORDER, 0); - - if ($matches === []) { + // check if a scheme is present at the start, if not we need to give it one + if (!preg_match('#^https?://#i', $trimmed)) { $trimmed = 'https://' . $trimmed; } From e323d665ef4b2c734debb6f589e3abde5ff85388 Mon Sep 17 00:00:00 2001 From: Fred Myerscough Date: Fri, 3 Apr 2026 13:05:13 +0100 Subject: [PATCH 03/11] fix(ping): fix IPv6 detection and add features - Fix IPv6 detection running after escapeshellarg - Consolidate OS detection into detectOs() method - Add setTimeout() and setTtl() fluent setters - Add isAlive() and latency() convenience methods - Add tests for new features --- src/PingUtility.php | 90 ++++++++++++++++++++++++-------- tests/PingUtility/ConfigTest.php | 72 +++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 23 deletions(-) create mode 100644 tests/PingUtility/ConfigTest.php diff --git a/src/PingUtility.php b/src/PingUtility.php index d68b7a2..9ae813b 100644 --- a/src/PingUtility.php +++ b/src/PingUtility.php @@ -27,7 +27,23 @@ public function __construct(string $url) } /** - * Ping a urls host + * Check if the host is alive + */ + public function isAlive(): bool + { + return $this->ping()['alive']; + } + + /** + * Get the latency in milliseconds, or null if unreachable + */ + public function latency(): ?float + { + return $this->ping()['latency']; + } + + /** + * Ping a urls host and return whether it's alive and the latency */ public function ping(): array { @@ -36,20 +52,15 @@ public function ping(): array 'latency' => null, ]; - $ttl = $this->ttl; - $timeout = $this->timeout; - $host = escapeshellarg($this->uriUtility->host()); - + $host = $this->uriUtility->host(); $pingCmd = $this->getPingCommand($host); + $escapedHost = escapeshellarg($host); - // Construct the ping command - if (str_starts_with(strtoupper(PHP_OS), 'WIN')) { - $exec_string = sprintf('%s -n 1 -i %d -w %d %s', $pingCmd, $ttl, $timeout * 1000, $host); - } elseif (strtoupper(PHP_OS) === 'DARWIN') { - $exec_string = sprintf('%s -c 1 -t %d %s', $pingCmd, $ttl, $host); - } else { - $exec_string = sprintf('%s -c 1 -t %d -w %d %s', $pingCmd, $ttl, $timeout, $host); - } + $exec_string = match ($this->detectOs()) { + 'WIN' => sprintf('%s -n 1 -i %d -w %d %s', $pingCmd, $this->ttl, $this->timeout * 1000, $escapedHost), + 'DARWIN' => sprintf('%s -c 1 -t %d %s', $pingCmd, $this->ttl, $escapedHost), + default => sprintf('%s -c 1 -t %d -w %d %s', $pingCmd, $this->ttl, $this->timeout, $escapedHost), + }; $output = []; @@ -60,9 +71,9 @@ public function ping(): array } foreach ($output as $line) { - if (preg_match('/time[=<]?\\s?(?