diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml
new file mode 100644
index 0000000..9600ee2
--- /dev/null
+++ b/.github/workflows/phpstan.yml
@@ -0,0 +1,9 @@
+name: PHPStan
+on:
+ pull_request:
+ push:
+ branches:
+ - 1.x
+jobs:
+ phpstan:
+ uses: artemeon/.shared/.github/workflows/phpstan-php84-upwards.yml@main
diff --git a/.github/workflows/pint.yml b/.github/workflows/pint.yml
new file mode 100644
index 0000000..39244fd
--- /dev/null
+++ b/.github/workflows/pint.yml
@@ -0,0 +1,27 @@
+name: Pint
+
+on:
+ - pull_request
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ pint:
+ name: Pint (PHP-CS-Fixer)
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Install PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: "8.4"
+ coverage: none
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Composer install
+ run: composer install --no-interaction --no-ansi --no-progress
+ - name: Run Pint
+ run: composer pint
diff --git a/.github/workflows/psalm_unit_tests.yml b/.github/workflows/tests.yml
similarity index 65%
rename from .github/workflows/psalm_unit_tests.yml
rename to .github/workflows/tests.yml
index e4a3280..ab3fded 100644
--- a/.github/workflows/psalm_unit_tests.yml
+++ b/.github/workflows/tests.yml
@@ -3,10 +3,10 @@ on:
pull_request:
push:
branches:
- - master
+ - 1.x
jobs:
- phpstan:
- name: PHPStan
+ tests:
+ name: Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -14,14 +14,14 @@ jobs:
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
- php-version: 8.1
- coverage: none
+ php-version: 8.4
+ coverage: xdebug
- name: Composer install
run: composer install --no-interaction --no-ansi --no-progress
- - name: Run PHPStan
- run: composer run phpstan
- phpunit:
- name: PHPUnit
+ - name: Run Tests
+ run: composer test:coverage
+ type_coverage:
+ name: Type Coverage
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -29,9 +29,9 @@ jobs:
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
- php-version: 8.1
- coverage: none
+ php-version: 8.4
+ coverage: xdebug
- name: Composer install
run: composer install --no-interaction --no-ansi --no-progress
- - name: Run PHPUnit
- run: vendor/phpunit/phpunit/phpunit -c tests/phpunit.xml
+ - name: Check Type Coverage
+ run: composer test:type-coverage
diff --git a/changelog.md b/changelog.md
index e99c121..96bd3cd 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,7 @@
+## 1.0.0 - 2025-01-20
+- Drop support for PHP 8.1, PHP 8.2 and PHP 8.3.
+- Add Support for PHP 8.4.
+
## 0.1.0 - 2020-02-12
* Initial release
diff --git a/composer.json b/composer.json
index a9cfb66..5cbafa2 100644
--- a/composer.json
+++ b/composer.json
@@ -1,16 +1,21 @@
{
"name": "artemeon/http-client",
+ "version": "1.1.0",
"description": "Thin wrapper for external http client libraries.",
"homepage": "https://github.com/artemeon/http-client#readme",
"license": "MIT",
"type": "library",
"keywords": [
- "php7",
"http",
"client"
],
"scripts": {
- "phpstan": "php ./vendor/bin/phpstan analyse --memory-limit=4G"
+ "phpstan": "php ./vendor/bin/phpstan analyse --memory-limit=4G",
+ "pint": "./vendor/bin/pint --test -v",
+ "pint:fix": "./vendor/bin/pint",
+ "test": "./vendor/bin/pest",
+ "test:coverage": "XDEBUG_MODE=coverage ./vendor/bin/pest --coverage --min=67.3",
+ "test:type-coverage": "XDEBUG_MODE=coverage ./vendor/bin/pest --type-coverage --min=99.8"
},
"authors": [
{
@@ -20,7 +25,8 @@
],
"config": {
"allow-plugins": {
- "phpstan/extension-installer": true
+ "phpstan/extension-installer": true,
+ "pestphp/pest-plugin": true
}
},
"autoload": {
@@ -34,22 +40,22 @@
}
},
"require": {
- "php": ">=8.1",
- "guzzlehttp/guzzle": "~7.4",
+ "php": ">=8.4",
+ "guzzlehttp/guzzle": "~7.9.2",
"ext-json": "*",
"ext-mbstring": "*",
"psr/log": "^1.1"
},
"require-dev": {
- "phpunit/phpunit": "9.*",
- "phpspec/prophecy-phpunit": "v2.*",
- "squizlabs/php_codesniffer": "3.*",
- "php-mock/php-mock-prophecy": "0.1.0",
+ "laravel/pint": "^1.20.0",
+ "mockery/mockery": "^1.6",
"mikey179/vfsstream": "1.6.*",
- "php-http/psr7-integration-tests": "1.1.*",
- "phpstan/phpstan": "^1.10",
+ "phpstan/phpstan": "^2.1.2",
"phpstan/extension-installer": "^1.3",
- "phpstan/phpstan-phpunit": "^1.3"
+ "phpstan/phpstan-phpunit": "^2.0.4",
+ "rector/rector": "^2.0.7",
+ "pestphp/pest": "^v3.7",
+ "pestphp/pest-plugin-type-coverage": "^3.2"
},
"minimum-stability": "stable"
}
diff --git a/phpstan.neon b/phpstan.neon
index 2eb2f74..60afcbd 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -2,7 +2,6 @@ includes:
- phar://phpstan.phar/conf/bleedingEdge.neon
parameters:
- level: 1
+ level: 5
paths:
- src
- - tests
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..0c12bb9
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ ./tests
+
+
+
+
+ ./src
+
+
+
diff --git a/pint.json b/pint.json
new file mode 100644
index 0000000..052aaec
--- /dev/null
+++ b/pint.json
@@ -0,0 +1,129 @@
+{
+ "preset": "psr12",
+ "rules": {
+ "align_multiline_comment": true,
+ "array_indentation": true,
+ "array_push": true,
+ "array_syntax": {
+ "syntax": "short"
+ },
+ "assign_null_coalescing_to_coalesce_equal": true,
+ "binary_operator_spaces": true,
+ "blank_line_before_statement": true,
+ "cast_spaces": true,
+ "clean_namespace": true,
+ "combine_consecutive_issets": true,
+ "combine_consecutive_unsets": true,
+ "compact_nullable_typehint": true,
+ "concat_space": {
+ "spacing": "one"
+ },
+ "fully_qualified_strict_types": true,
+ "function_to_constant": true,
+ "get_class_to_class_keyword": true,
+ "is_null": true,
+ "lambda_not_used_import": true,
+ "logical_operators": true,
+ "method_chaining_indentation": true,
+ "modernize_types_casting": true,
+ "multiline_whitespace_before_semicolons": true,
+ "no_empty_comment": true,
+ "no_empty_phpdoc": true,
+ "no_empty_statement": true,
+ "no_extra_blank_lines": {
+ "tokens": [
+ "attribute",
+ "break",
+ "case",
+ "continue",
+ "curly_brace_block",
+ "default",
+ "extra",
+ "parenthesis_brace_block",
+ "return",
+ "square_brace_block",
+ "switch",
+ "throw",
+ "use",
+ "use_trait"
+ ]
+ },
+ "no_multiline_whitespace_around_double_arrow": true,
+ "no_short_bool_cast": true,
+ "no_singleline_whitespace_before_semicolons": true,
+ "no_superfluous_elseif": false,
+ "no_superfluous_phpdoc_tags": true,
+ "no_trailing_comma_in_singleline": true,
+ "no_unneeded_control_parentheses": true,
+ "no_useless_concat_operator": true,
+ "no_useless_else": true,
+ "no_useless_nullsafe_operator": true,
+ "no_useless_return": true,
+ "no_whitespace_before_comma_in_array": true,
+ "not_operator_with_successor_space": false,
+ "nullable_type_declaration": true,
+ "object_operator_without_whitespace": true,
+ "ordered_imports": {
+ "imports_order": [
+ "class",
+ "function",
+ "const"
+ ],
+ "sort_algorithm": "alpha"
+ },
+ "ordered_interfaces": true,
+ "ordered_types": {
+ "null_adjustment": "always_last"
+ },
+ "phpdoc_align": {
+ "align": "left"
+ },
+ "phpdoc_indent": true,
+ "phpdoc_no_useless_inheritdoc": true,
+ "phpdoc_order": true,
+ "phpdoc_scalar": true,
+ "phpdoc_single_line_var_spacing": true,
+ "phpdoc_summary": true,
+ "phpdoc_tag_casing": true,
+ "phpdoc_trim": true,
+ "phpdoc_trim_consecutive_blank_line_separation": true,
+ "phpdoc_var_without_name": true,
+ "php_unit_construct": true,
+ "php_unit_dedicate_assert": true,
+ "php_unit_dedicate_assert_internal_type": true,
+ "php_unit_internal_class": true,
+ "php_unit_method_casing": true,
+ "return_assignment": true,
+ "return_type_declaration": true,
+ "short_scalar_cast": true,
+ "single_line_comment_spacing": true,
+ "single_line_comment_style": true,
+ "single_quote": true,
+ "single_space_around_construct": true,
+ "ternary_to_null_coalescing": true,
+ "trailing_comma_in_multiline": {
+ "elements": [
+ "arguments",
+ "arrays",
+ "match",
+ "parameters"
+ ]
+ },
+ "trim_array_spaces": true,
+ "type_declaration_spaces": true,
+ "types_spaces": {
+ "space": "single"
+ },
+ "use_arrow_functions": false,
+ "void_return": true,
+ "whitespace_after_comma_in_array": {
+ "ensure_single_space": true
+ },
+ "yoda_style": {
+ "equal": false,
+ "identical": false,
+ "less_and_greater": false,
+ "always_move_variable": false
+ }
+ }
+}
diff --git a/rector.php b/rector.php
new file mode 100644
index 0000000..0d862f3
--- /dev/null
+++ b/rector.php
@@ -0,0 +1,36 @@
+withPhpSets()
+ ->withAttributesSets(phpunit: true)
+ ->withRules([
+ Rector\CodeQuality\Rector\Ternary\ArrayKeyExistsTernaryThenValueToCoalescingRector::class,
+ Rector\CodeQuality\Rector\NullsafeMethodCall\CleanupUnneededNullsafeOperatorRector::class,
+ Rector\CodeQuality\Rector\ClassMethod\InlineArrayReturnAssignRector::class,
+ Rector\CodeQuality\Rector\Ternary\UnnecessaryTernaryExpressionRector::class,
+ Rector\DeadCode\Rector\Foreach_\RemoveUnusedForeachKeyRector::class,
+ Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromStrictFluentReturnRector::class,
+ Rector\Php80\Rector\Class_\StringableForToStringRector::class,
+ Rector\CodingStyle\Rector\ArrowFunction\StaticArrowFunctionRector::class,
+ Rector\CodingStyle\Rector\Closure\StaticClosureRector::class,
+ Rector\DeadCode\Rector\Node\RemoveNonExistingVarAnnotationRector::class,
+ Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodParameterRector::class,
+ Rector\TypeDeclaration\Rector\ClassMethod\BoolReturnTypeFromBooleanStrictReturnsRector::class,
+ Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromReturnNewRector::class,
+ Rector\TypeDeclaration\Rector\ClassMethod\ParamTypeByMethodCallTypeRector::class,
+ Rector\TypeDeclaration\Rector\ClassMethod\NumericReturnTypeFromStrictScalarReturnsRector::class,
+ Rector\TypeDeclaration\Rector\ClassMethod\AddMethodCallBasedStrictParamTypeRector::class,
+ Rector\CodeQuality\Rector\If_\ExplicitBoolCompareRector::class,
+ Rector\CodeQuality\Rector\Foreach_\ForeachItemsAssignToEmptyArrayToAssignRector::class,
+ Rector\CodeQuality\Rector\Foreach_\ForeachToInArrayRector::class,
+ Rector\CodeQuality\Rector\BooleanAnd\RemoveUselessIsObjectCheckRector::class,
+ ])
+ ->withPaths([
+ __DIR__ . '/src',
+ __DIR__ . '/tests',
+ ])
+ ->withTypeCoverageLevel(0);
diff --git a/src/Client/ArtemeonHttpClient.php b/src/Client/ArtemeonHttpClient.php
index 6d45df2..2542ca8 100644
--- a/src/Client/ArtemeonHttpClient.php
+++ b/src/Client/ArtemeonHttpClient.php
@@ -38,23 +38,22 @@
use GuzzleHttp\Exception\ServerException as GuzzleServerException;
use GuzzleHttp\Exception\TooManyRedirectsException as GuzzleTooManyRedirectsException;
use GuzzleHttp\Exception\TransferException as GuzzleTransferException;
+use Override;
use Psr\Http\Message\ResponseInterface as GuzzleResponse;
/**
- * HttpClient implementation with guzzle
+ * HttpClient implementation with guzzle.
*/
class ArtemeonHttpClient implements HttpClient
{
- private GuzzleClient $guzzleClient;
- private ClientOptionsConverter $clientOptionsConverter;
-
- public function __construct(GuzzleClient $guzzleClient, ClientOptionsConverter $clientOptionsConverter)
- {
- $this->guzzleClient = $guzzleClient;
- $this->clientOptionsConverter = $clientOptionsConverter;
+ public function __construct(
+ private readonly GuzzleClient $guzzleClient,
+ private readonly ClientOptionsConverter $clientOptionsConverter,
+ ) {
}
- final public function send(Request $request, ClientOptions $clientOptions = null): Response
+ #[Override]
+ final public function send(Request $request, ?ClientOptions $clientOptions = null): Response
{
if ($clientOptions instanceof ClientOptions) {
$guzzleOptions = $this->clientOptionsConverter->toGuzzleOptionsArray($clientOptions);
@@ -116,7 +115,7 @@ private function doSend(Request $request, array $guzzleOptions): Response
}
/**
- * Checks the Guzzle exception for a response object and converts it to a Artemeon response object
+ * Checks the Guzzle exception for a response object and converts it to a Artemeon response object.
*/
private function getResponseFromGuzzleException(GuzzleRequestException $guzzleRequestException): ?Response
{
@@ -128,9 +127,9 @@ private function getResponseFromGuzzleException(GuzzleRequestException $guzzleRe
}
/**
- * Converts a GuzzleResponse object to our Response object
+ * Converts a GuzzleResponse object to our Response object.
*/
- private function convertGuzzleResponse(GuzzleResponse $guzzleResponse): Response
+ private function convertGuzzleResponse(?GuzzleResponse $guzzleResponse): Response
{
$headers = Headers::create();
@@ -143,7 +142,7 @@ private function convertGuzzleResponse(GuzzleResponse $guzzleResponse): Response
$guzzleResponse->getProtocolVersion(),
$guzzleResponse->getBody(),
$headers,
- $guzzleResponse->getReasonPhrase()
+ $guzzleResponse->getReasonPhrase(),
);
}
}
diff --git a/src/Client/Decorator/ClientOptionsModifier/HttpClientWithModifiedOptions.php b/src/Client/Decorator/ClientOptionsModifier/HttpClientWithModifiedOptions.php
index 48dbcdf..12dadc1 100644
--- a/src/Client/Decorator/ClientOptionsModifier/HttpClientWithModifiedOptions.php
+++ b/src/Client/Decorator/ClientOptionsModifier/HttpClientWithModifiedOptions.php
@@ -10,18 +10,17 @@
use Artemeon\HttpClient\Client\Options\ClientOptionsModifier;
use Artemeon\HttpClient\Http\Request;
use Artemeon\HttpClient\Http\Response;
+use Override;
final class HttpClientWithModifiedOptions extends HttpClientDecorator
{
- private ClientOptionsModifier $clientOptionsModifier;
-
- public function __construct(HttpClient $httpClient, ClientOptionsModifier $clientOptionsModifier)
+ public function __construct(HttpClient $httpClient, private readonly ClientOptionsModifier $clientOptionsModifier)
{
parent::__construct($httpClient);
- $this->clientOptionsModifier = $clientOptionsModifier;
}
- public function send(Request $request, ClientOptions $clientOptions = null): Response
+ #[Override]
+ public function send(Request $request, ?ClientOptions $clientOptions = null): Response
{
return $this->httpClient->send($request, $this->modified($clientOptions));
}
diff --git a/src/Client/Decorator/HttpClientDecorator.php b/src/Client/Decorator/HttpClientDecorator.php
index ab77aff..8d41ccc 100644
--- a/src/Client/Decorator/HttpClientDecorator.php
+++ b/src/Client/Decorator/HttpClientDecorator.php
@@ -17,26 +17,23 @@
use Artemeon\HttpClient\Client\Options\ClientOptions;
use Artemeon\HttpClient\Http\Request;
use Artemeon\HttpClient\Http\Response;
+use Override;
/**
- * Abstract base class for the decorator pattern
+ * Abstract base class for the decorator pattern.
*/
abstract class HttpClientDecorator implements HttpClient
{
- protected HttpClient $httpClient;
-
/**
* HttpClientDecorator constructor.
- *
- * @param HttpClient $httpClient
*/
- public function __construct(HttpClient $httpClient)
+ public function __construct(protected HttpClient $httpClient)
{
- $this->httpClient = $httpClient;
}
/**
* @inheritDoc
*/
- abstract public function send(Request $request, ClientOptions $clientOptions = null): Response;
+ #[Override]
+ abstract public function send(Request $request, ?ClientOptions $clientOptions = null): Response;
}
diff --git a/src/Client/Decorator/Logger/LoggerDecorator.php b/src/Client/Decorator/Logger/LoggerDecorator.php
index cb7ef10..d187b2f 100644
--- a/src/Client/Decorator/Logger/LoggerDecorator.php
+++ b/src/Client/Decorator/Logger/LoggerDecorator.php
@@ -21,33 +21,34 @@
use Artemeon\HttpClient\Exception\Request\Http\ServerResponseException;
use Artemeon\HttpClient\Http\Request;
use Artemeon\HttpClient\Http\Response;
+use Override;
use Psr\Log\LoggerInterface;
/**
- * Decorator class to add Psr logging to the httpClient
+ * Decorator class to add Psr logging to the httpClient.
*/
class LoggerDecorator extends HttpClientDecorator
{
- private LoggerInterface $logger;
-
- public function __construct(HttpClient $httpClient, LoggerInterface $logger)
+ public function __construct(HttpClient $httpClient, private readonly LoggerInterface $logger)
{
- $this->logger = $logger;
parent::__construct($httpClient);
}
/**
* @inheritDoc
*/
- public function send(Request $request, ClientOptions $clientOptions = null): Response
+ #[Override]
+ public function send(Request $request, ?ClientOptions $clientOptions = null): Response
{
try {
return $this->httpClient->send($request, $clientOptions);
} catch (ClientResponseException | ServerResponseException $exception) {
$this->logger->error($exception->getMessage(), ['exception' => $exception]);
+
throw $exception;
} catch (HttpClientException $exception) {
$this->logger->info($exception->getMessage(), ['exception' => $exception]);
+
throw $exception;
}
}
diff --git a/src/Client/Decorator/OAuth2/ClientCredentials.php b/src/Client/Decorator/OAuth2/ClientCredentials.php
index fe9684e..a3df52e 100644
--- a/src/Client/Decorator/OAuth2/ClientCredentials.php
+++ b/src/Client/Decorator/OAuth2/ClientCredentials.php
@@ -14,13 +14,13 @@
namespace Artemeon\HttpClient\Client\Decorator\OAuth2;
/**
- * Class to generate client credentials for OAuth2 Access Token Request's
+ * Class to generate client credentials for OAuth2 Access Token Request's.
*/
class ClientCredentials
{
- private string $clientId;
- private string $clientSecret;
- private string $scope;
+ private readonly string $clientId;
+ private readonly string $clientSecret;
+ private readonly string $scope;
/**
* ClientCredentials constructor.
@@ -38,9 +38,9 @@ private function __construct(string $clientID, string $clientSecret, string $sco
}
/**
- * Named constructor to create an instance based on the given credentials
+ * Named constructor to create an instance based on the given credentials.
*
- * @param string $clientId The 'client_id'
+ * @param string $clientId The 'client_id'
* @param string $clientSecret The 'client_secret'
* @param string $scope The scope
*/
@@ -50,7 +50,7 @@ public static function forHeaderAuthorization(string $clientId, string $clientSe
}
/**
- * Creates the required key => value pairs for the Access Token Request
+ * Creates the required key => value pairs for the Access Token Request.
*/
public function toArray(): array
{
diff --git a/src/Client/Decorator/OAuth2/ClientCredentialsDecorator.php b/src/Client/Decorator/OAuth2/ClientCredentialsDecorator.php
index 017ed2c..682e8b9 100644
--- a/src/Client/Decorator/OAuth2/ClientCredentialsDecorator.php
+++ b/src/Client/Decorator/OAuth2/ClientCredentialsDecorator.php
@@ -31,6 +31,7 @@
use Artemeon\HttpClient\Http\Response;
use Artemeon\HttpClient\Http\Uri;
use Exception;
+use Override;
/**
* Http client decorator to add transparent access tokens to requests. Fetches the 'Access Token' from
@@ -38,9 +39,6 @@
*/
class ClientCredentialsDecorator extends HttpClientDecorator
{
- private Request $accessTokenRequest;
- private AccessTokenCache $accessTokenCache;
-
/**
* ClientCredentialsDecorator constructor.
*
@@ -50,17 +48,14 @@ class ClientCredentialsDecorator extends HttpClientDecorator
*/
public function __construct(
HttpClient $httpClient,
- Request $accessTokenRequest,
- AccessTokenCache $accessTokenCache
+ private readonly Request $accessTokenRequest,
+ private readonly AccessTokenCache $accessTokenCache,
) {
- $this->accessTokenRequest = $accessTokenRequest;
- $this->accessTokenCache = $accessTokenCache;
-
parent::__construct($httpClient);
}
/**
- * Named constructor to create an instance based on the given ClientCredentials
+ * Named constructor to create an instance based on the given ClientCredentials.
*
* @param ClientCredentials $clientCredentials The OAuth2 client credential object
* @param Uri $uri The Uri object
@@ -74,7 +69,7 @@ public static function fromClientCredentials(
ClientCredentials $clientCredentials,
Uri $uri,
HttpClient $httpClient,
- AccessTokenCache $accessTokenCache = null
+ ?AccessTokenCache $accessTokenCache = null,
): self {
// Ensure default cache strategy
if ($accessTokenCache === null) {
@@ -90,7 +85,8 @@ public static function fromClientCredentials(
/**
* @inheritDoc
*/
- public function send(Request $request, ClientOptions $clientOptions = null): Response
+ #[Override]
+ public function send(Request $request, ?ClientOptions $clientOptions = null): Response
{
if ($this->accessTokenCache->isExpired()) {
$this->accessTokenCache->add($this->requestAccessToken());
@@ -104,17 +100,16 @@ public function send(Request $request, ClientOptions $clientOptions = null): Res
}
/**
- * Fetches the access token
+ * Fetches the access token.
*
- * @param ClientOptions|null $clientOptions
* @throws RuntimeException
*/
- private function requestAccessToken(ClientOptions $clientOptions = null): AccessToken
+ private function requestAccessToken(?ClientOptions $clientOptions = null): AccessToken
{
try {
$response = $this->httpClient->send($this->accessTokenRequest, $clientOptions);
- } catch (HttpClientException | Exception $exception) {
- throw new RuntimeException("Cant request access token", 0, $exception);
+ } catch (Exception | HttpClientException $exception) {
+ throw new RuntimeException('Cant request access token', 0, $exception);
}
$this->assertIsValidJsonResponse($response);
@@ -123,9 +118,8 @@ private function requestAccessToken(ClientOptions $clientOptions = null): Access
}
/**
- * Checks for a valid access token response with valid json body
+ * Checks for a valid access token response with valid json body.
*
- * @param Response $response
* @throws RuntimeException
*/
private function assertIsValidJsonResponse(Response $response): void
@@ -133,10 +127,10 @@ private function assertIsValidJsonResponse(Response $response): void
if ($response->getStatusCode() !== 200) {
throw new RuntimeException(
sprintf(
- "Invalid status code: s% for access token request, Body: %s",
+ 'Invalid status code: s% for access token request, Body: %s',
$response->getStatusCode(),
- $response->getBody()->__toString()
- )
+ $response->getBody()->__toString(),
+ ),
);
}
diff --git a/src/Client/Decorator/OAuth2/Token/AccessToken.php b/src/Client/Decorator/OAuth2/Token/AccessToken.php
index 8c2fd33..3e55b14 100644
--- a/src/Client/Decorator/OAuth2/Token/AccessToken.php
+++ b/src/Client/Decorator/OAuth2/Token/AccessToken.php
@@ -16,14 +16,13 @@
use Artemeon\HttpClient\Exception\RuntimeException;
/**
- * Class to describe a OAuth2 access token
+ * Class to describe a OAuth2 access token.
*/
class AccessToken
{
- private string $token;
- private int $expires;
- private string $type;
- private string $scope;
+ private readonly string $token;
+ private readonly int $expires;
+ private readonly string $type;
/**
* AccessToken constructor.
@@ -34,22 +33,21 @@ class AccessToken
* @param string $scope The scope of the authorization
* @throws RuntimeException
*/
- private function __construct(string $token, int $expires, string $type, string $scope = '')
+ private function __construct(string $token, int $expires, string $type, private readonly string $scope = '')
{
if (empty($token) || empty($expires) || empty($type)) {
throw new RuntimeException(
- "Access token fields: 'access_token', 'expires_in', 'token_type' are mandatory"
+ "Access token fields: 'access_token', 'expires_in', 'token_type' are mandatory",
);
}
$this->token = $token;
$this->expires = $expires;
$this->type = $type;
- $this->scope = $scope;
}
/**
- * Named constructor to create an instance based on the given json encoded body string
+ * Named constructor to create an instance based on the given json encoded body string.
*
* @param string $json Json encoded response string
* @throws RuntimeException
@@ -59,15 +57,15 @@ public static function fromJsonString(string $json): self
$data = json_decode($json, true);
return new self(
- (string) $data['access_token'] ?? '',
- (int) $data['expires_in'] ?? 0,
- (string) $data['token_type'] ?? '',
- (string) $data['scope'] ?? ''
+ (string) ($data['access_token'] ?? ''),
+ (int) ($data['expires_in'] ?? 0),
+ (string) ($data['token_type'] ?? ''),
+ (string) ($data['scope'] ?? ''),
);
}
/**
- * Returns the access token string
+ * Returns the access token string.
*/
public function getToken(): string
{
@@ -75,7 +73,7 @@ public function getToken(): string
}
/**
- * Returns the expires in integer value
+ * Returns the expires in integer value.
*/
public function getExpires(): int
{
@@ -83,7 +81,7 @@ public function getExpires(): int
}
/**
- * Return the type of the authorization
+ * Return the type of the authorization.
*/
public function getType(): string
{
@@ -91,7 +89,7 @@ public function getType(): string
}
/**
- * Return the scope of the authorization
+ * Return the scope of the authorization.
*/
public function getScope(): string
{
diff --git a/src/Client/Decorator/OAuth2/Token/AccessTokenCache.php b/src/Client/Decorator/OAuth2/Token/AccessTokenCache.php
index 90aabf5..e887c91 100644
--- a/src/Client/Decorator/OAuth2/Token/AccessTokenCache.php
+++ b/src/Client/Decorator/OAuth2/Token/AccessTokenCache.php
@@ -16,27 +16,24 @@
use Artemeon\HttpClient\Exception\RuntimeException;
/**
- * Interface to realize several strategy's to store AccessToken
+ * Interface to realize several strategy's to store AccessToken.
*/
interface AccessTokenCache
{
/**
- * Add token to the cache
- *
- * @param AccessToken $accessToken
+ * Add token to the cache.
*/
- public function add(AccessToken $accessToken);
+ public function add(AccessToken $accessToken): void;
/**
- * Get token from the cache
+ * Get token from the cache.
*
* @throws RuntimeException
*/
public function get(): AccessToken;
/**
- * Check if the token is expired or not set
+ * Check if the token is expired or not set.
*/
public function isExpired(): bool;
-
}
diff --git a/src/Client/Decorator/OAuth2/Token/InMemoryAccessTokenCache.php b/src/Client/Decorator/OAuth2/Token/InMemoryAccessTokenCache.php
index 67e4fa7..8af4886 100644
--- a/src/Client/Decorator/OAuth2/Token/InMemoryAccessTokenCache.php
+++ b/src/Client/Decorator/OAuth2/Token/InMemoryAccessTokenCache.php
@@ -14,9 +14,10 @@
namespace Artemeon\HttpClient\Client\Decorator\OAuth2\Token;
use Artemeon\HttpClient\Exception\RuntimeException;
+use Override;
/**
- * Class to store AccessToken in memory
+ * Class to store AccessToken in memory.
*/
class InMemoryAccessTokenCache implements AccessTokenCache
{
@@ -26,7 +27,8 @@ class InMemoryAccessTokenCache implements AccessTokenCache
/**
* @inheritDoc
*/
- public function add(AccessToken $accessToken)
+ #[Override]
+ public function add(AccessToken $accessToken): void
{
$this->token = $accessToken;
$this->expireTime = time() + $accessToken->getExpires();
@@ -35,6 +37,7 @@ public function add(AccessToken $accessToken)
/**
* @inheritDoc
*/
+ #[Override]
public function get(): AccessToken
{
if ($this->token === null) {
@@ -51,6 +54,7 @@ public function get(): AccessToken
/**
* @inheritDoc
*/
+ #[Override]
public function isExpired(): bool
{
if (!$this->token instanceof AccessToken) {
diff --git a/src/Client/HttpClient.php b/src/Client/HttpClient.php
index 15afe93..9016b4a 100644
--- a/src/Client/HttpClient.php
+++ b/src/Client/HttpClient.php
@@ -27,17 +27,17 @@
use Artemeon\HttpClient\Http\Response;
/**
- * Interface to plug in third party http-client libraries
+ * Interface to plug in third party http-client libraries.
*/
interface HttpClient
{
/**
- * Sends the request
+ * Sends the request.
*
* @param Request $request Request object to send
* @param ClientOptions|null $clientOptions Optional client configuration object
*
- * @throws HttpClientException Interface to catch all possible exceptions
+ * @throws HttpClientException Interface to catch all possible exceptions
* @throws InvalidArgumentException 1. All exceptions with invalid arguments
* @throws RuntimeException 2. All exceptions during runtime
* @throws TransferException 2.1 All exceptions during the request/response transfers
@@ -48,5 +48,5 @@ interface HttpClient
* @throws RedirectResponseException 2.1.2.3 All response exceptions with 300 status codes
* @throws \InvalidArgumentException
*/
- public function send(Request $request, ClientOptions $clientOptions = null): Response;
+ public function send(Request $request, ?ClientOptions $clientOptions = null): Response;
}
diff --git a/src/Client/HttpClientFactory.php b/src/Client/HttpClientFactory.php
index d42a10b..31163f5 100644
--- a/src/Client/HttpClientFactory.php
+++ b/src/Client/HttpClientFactory.php
@@ -24,24 +24,24 @@
use Psr\Log\LoggerInterface;
/**
- * Static factory class for production environment
+ * Static factory class for production environment.
*/
class HttpClientFactory
{
/**
- * Named constructor to create an instance for production environments without logging
+ * Named constructor to create an instance for production environments without logging.
*/
public static function create(): HttpClient
{
return new ArtemeonHttpClient(
new GuzzleClient(),
- new ClientOptionsConverter()
+ new ClientOptionsConverter(),
);
}
/**
* Named constructor to create an instance for production with a PSR 3 logger for
- * request/response calls and all occurred exceptions
+ * request/response calls and all occurred exceptions.
*
* @param LoggerInterface $logger PSR-3 logger @see https://www.php-fig.org/psr/psr-3/
* @param string $format @see \GuzzleHttp\MessageFormatter for all allowed options
@@ -55,7 +55,7 @@ public static function withLogger(LoggerInterface $logger, string $format = '{re
$handlerStack->push(Middleware::log($logger, $formatter));
$httpClient = new ArtemeonHttpClient(
new GuzzleClient(['handler' => $handlerStack]),
- new ClientOptionsConverter()
+ new ClientOptionsConverter(),
);
} catch (InvalidArgumentException $exception) {
throw RuntimeException::fromPreviousException($exception);
diff --git a/src/Client/HttpClientTestFactory.php b/src/Client/HttpClientTestFactory.php
index ac5cae9..c714b0e 100644
--- a/src/Client/HttpClientTestFactory.php
+++ b/src/Client/HttpClientTestFactory.php
@@ -29,7 +29,7 @@
class HttpClientTestFactory
{
private array $transactionLog = [];
- private MockHandler $mockHandler;
+ private readonly MockHandler $mockHandler;
private static HttpClientTestFactory $instance;
public function __construct()
@@ -39,7 +39,7 @@ public function __construct()
}
/**
- * Singleton factory
+ * Singleton factory.
*/
private static function getInstance(): self
{
@@ -51,7 +51,7 @@ private static function getInstance(): self
}
/**
- * Named constructor to create an instance with a middleware to record transactions only for debugging purposes
+ * Named constructor to create an instance with a middleware to record transactions only for debugging purposes.
*
* Example:
* $httpClient = HttpClientTestFactory::withTransactionLog();
@@ -69,7 +69,7 @@ public static function withTransactionLog(): HttpClient
return new ArtemeonHttpClient(
new GuzzleClient(['handler' => $handlerStack]),
- new ClientOptionsConverter()
+ new ClientOptionsConverter(),
);
} catch (InvalidArgumentException $exception) {
throw RuntimeException::fromPreviousException($exception);
@@ -77,7 +77,7 @@ public static function withTransactionLog(): HttpClient
}
/**
- * Creates an instance to mock
+ * Creates an instance to mock.
*
* ```php
* HttpClientTestFactory::mockResponses(
@@ -101,7 +101,7 @@ public static function withMockHandler(): HttpClient
return new ArtemeonHttpClient(
new GuzzleClient(['handler' => HandlerStack::create($instance->mockHandler)]),
- new ClientOptionsConverter()
+ new ClientOptionsConverter(),
);
} catch (InvalidArgumentException $exception) {
throw RuntimeException::fromPreviousException($exception);
@@ -109,9 +109,8 @@ public static function withMockHandler(): HttpClient
}
/**
- * Register the responses to mock
+ * Register the responses to mock.
*
- * @param array $responses
* @throws InvalidArgumentException
*/
public static function mockResponses(array $responses): void
@@ -128,7 +127,7 @@ public static function mockResponses(array $responses): void
}
/**
- * Return the recorded transaction log array
+ * Return the recorded transaction log array.
*/
public static function getTransactionLog(): array
{
@@ -136,7 +135,7 @@ public static function getTransactionLog(): array
}
/**
- * Returns the formatted transaction logs as a string
+ * Returns the formatted transaction logs as a string.
*/
public static function printTransactionLog(): void
{
diff --git a/src/Client/Options/ClientOptions.php b/src/Client/Options/ClientOptions.php
index 092f8ee..e2b45e3 100644
--- a/src/Client/Options/ClientOptions.php
+++ b/src/Client/Options/ClientOptions.php
@@ -14,7 +14,7 @@
namespace Artemeon\HttpClient\Client\Options;
/**
- * Class with all possible client configuration options on the network layer
+ * Class with all possible client configuration options on the network layer.
*/
class ClientOptions
{
@@ -25,8 +25,8 @@ class ClientOptions
private int $maxRedirects;
private bool $addReferer;
- /** @var resource */
- private $sink;
+ /** @var resource|null */
+ private mixed $sink = null;
private bool $httpErrors;
@@ -34,7 +34,7 @@ class ClientOptions
private $handler;
/**
- * Named constructor to create an instance based on the default values
+ * Named constructor to create an instance based on the default values.
*/
public static function fromDefaults(): self
{
@@ -61,7 +61,7 @@ public function optDisableRedirects(): void
}
/**
- * Is redirect allowed
+ * Is redirect allowed.
*/
public function isRedirectAllowed(): bool
{
@@ -69,7 +69,7 @@ public function isRedirectAllowed(): bool
}
/**
- * Option to disable SSL certificate verification
+ * Option to disable SSL certificate verification.
*/
public function optDisableSslVerification(): void
{
@@ -77,7 +77,7 @@ public function optDisableSslVerification(): void
}
/**
- * Is SSL certificate verification enabled
+ * Is SSL certificate verification enabled.
*/
public function isSslVerificationEnabled(): bool
{
@@ -87,8 +87,6 @@ public function isSslVerificationEnabled(): bool
/**
* Option to set a custom CA bundle certificates path. As default we use the CA bundle
* provided by the operating system.
- *
- * @param string $customCaBundlePath
*/
public function optSetCustomCaBundlePath(string $customCaBundlePath): void
{
@@ -96,7 +94,7 @@ public function optSetCustomCaBundlePath(string $customCaBundlePath): void
}
/**
- * Returns the custom CA bundle path or an empty string (Default)
+ * Returns the custom CA bundle path or an empty string (Default).
*/
public function getCustomCaBundlePath(): string
{
@@ -104,9 +102,7 @@ public function getCustomCaBundlePath(): string
}
/**
- * Option to set the timeout in seconds for requests
- *
- * @param int $timeout
+ * Option to set the timeout in seconds for requests.
*/
public function optSetTimeout(int $timeout): void
{
@@ -114,7 +110,7 @@ public function optSetTimeout(int $timeout): void
}
/**
- * Returns the connect timeout
+ * Returns the connect timeout.
*/
public function getTimeout(): int
{
@@ -122,9 +118,7 @@ public function getTimeout(): int
}
/**
- * Option to set the amount of maximal allowed redirects
- *
- * @param int $maxRedirects
+ * Option to set the amount of maximal allowed redirects.
*/
public function optSetMaxRedirects(int $maxRedirects): void
{
@@ -132,7 +126,7 @@ public function optSetMaxRedirects(int $maxRedirects): void
}
/**
- * Returns the amount of max allowed redirects
+ * Returns the amount of max allowed redirects.
*/
public function getMaxAllowedRedirects(): int
{
@@ -140,7 +134,7 @@ public function getMaxAllowedRedirects(): int
}
/**
- * Option to disable the referer for redirects
+ * Option to disable the referer for redirects.
*/
public function optDisableRefererForRedirects(): void
{
@@ -166,15 +160,15 @@ public function hasCustomCaBundlePath(): bool
/**
* @param resource $sink
*/
- public function setSink($sink): void
+ public function setSink(mixed $sink): void
{
$this->sink = $sink;
}
/**
- * @return resource
+ * @return resource|null
*/
- public function getSink()
+ public function getSink(): mixed
{
return $this->sink;
}
diff --git a/src/Client/Options/ClientOptionsConverter.php b/src/Client/Options/ClientOptionsConverter.php
index 44f3e46..f77c7c9 100644
--- a/src/Client/Options/ClientOptionsConverter.php
+++ b/src/Client/Options/ClientOptionsConverter.php
@@ -16,14 +16,12 @@
use GuzzleHttp\RequestOptions as GuzzleRequestOptions;
/**
- * Class to convert http-client options object to the guzzle options array format
+ * Class to convert http-client options object to the guzzle options array format.
*/
class ClientOptionsConverter
{
/**
- * Converts the given ClientOptions to the guzzle options array format
- *
- * @param ClientOptions $clientOptions
+ * Converts the given ClientOptions to the guzzle options array format.
*/
public function toGuzzleOptionsArray(ClientOptions $clientOptions): array
{
@@ -46,34 +44,32 @@ public function toGuzzleOptionsArray(ClientOptions $clientOptions): array
/**
* @see http://docs.guzzlephp.org/en/6.5/request-options.html#verify
- * @param ClientOptions $clientOptions
- * @return string|bool
*/
- private function createVerifyKey(ClientOptions $clientOptions)
+ private function createVerifyKey(ClientOptions $clientOptions): bool | string
{
if ($clientOptions->isSslVerificationEnabled()) {
if ($clientOptions->hasCustomCaBundlePath()) {
return $clientOptions->getCustomCaBundlePath();
- } else {
- return true;
}
+
+ return true;
}
+
return false;
}
/**
* @see http://docs.guzzlephp.org/en/6.5/request-options.html#allow-redirects
- * @param ClientOptions $clientOptions
- * @return array|bool
*/
- private function createAllowRedirectsKey(ClientOptions $clientOptions)
+ private function createAllowRedirectsKey(ClientOptions $clientOptions): array | false
{
- if ($clientOptions->isRedirectAllowed()) {
- return [
- 'max' => $clientOptions->getMaxAllowedRedirects(),
- 'referer' => $clientOptions->isRefererForRedirectsEnabled(),
- ];
+ if (!$clientOptions->isRedirectAllowed()) {
+ return false;
}
- return false;
+
+ return [
+ 'max' => $clientOptions->getMaxAllowedRedirects(),
+ 'referer' => $clientOptions->isRefererForRedirectsEnabled(),
+ ];
}
}
diff --git a/src/Client/Options/InlineClientOptionsModifier.php b/src/Client/Options/InlineClientOptionsModifier.php
index 1ee03a9..5241662 100644
--- a/src/Client/Options/InlineClientOptionsModifier.php
+++ b/src/Client/Options/InlineClientOptionsModifier.php
@@ -5,14 +5,12 @@
namespace Artemeon\HttpClient\Client\Options;
use Closure;
+use Override;
-final class InlineClientOptionsModifier implements ClientOptionsModifier
+final readonly class InlineClientOptionsModifier implements ClientOptionsModifier
{
- private Closure $callback;
-
- private function __construct(Closure $callback)
+ private function __construct(private Closure $callback)
{
- $this->callback = $callback;
}
public static function fromClosure(Closure $closure): self
@@ -25,6 +23,7 @@ public static function fromCallable(callable $callable): self
return new self(Closure::fromCallable($callable));
}
+ #[Override]
public function modify(ClientOptions $clientOptions): ClientOptions
{
$callback = $this->callback;
diff --git a/src/Exception/HttpClientException.php b/src/Exception/HttpClientException.php
index e4558ea..51549b2 100644
--- a/src/Exception/HttpClientException.php
+++ b/src/Exception/HttpClientException.php
@@ -7,7 +7,7 @@
use Throwable;
/**
- * Interface to catch all possible HttpClient exceptions
+ * Interface to catch all possible HttpClient exceptions.
*/
interface HttpClientException extends Throwable
{
diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php
index af7b7ef..abbbe1f 100644
--- a/src/Exception/InvalidArgumentException.php
+++ b/src/Exception/InvalidArgumentException.php
@@ -5,8 +5,7 @@
namespace Artemeon\HttpClient\Exception;
/**
- * Exception class for all invalid argument exceptions
- *
+ * Exception class for all invalid argument exceptions.
*/
class InvalidArgumentException extends \InvalidArgumentException implements HttpClientException
{
diff --git a/src/Exception/Request/Http/ClientResponseException.php b/src/Exception/Request/Http/ClientResponseException.php
index aff06bc..8516c5f 100644
--- a/src/Exception/Request/Http/ClientResponseException.php
+++ b/src/Exception/Request/Http/ClientResponseException.php
@@ -14,8 +14,8 @@
namespace Artemeon\HttpClient\Exception\Request\Http;
/**
- * Exception class to catch all client related http errors (400 range)
+ * Exception class to catch all client related http errors (400 range).
*/
class ClientResponseException extends ResponseException
{
-}
\ No newline at end of file
+}
diff --git a/src/Exception/Request/Http/RedirectResponseException.php b/src/Exception/Request/Http/RedirectResponseException.php
index 2d893de..dbfb1d7 100644
--- a/src/Exception/Request/Http/RedirectResponseException.php
+++ b/src/Exception/Request/Http/RedirectResponseException.php
@@ -14,8 +14,8 @@
namespace Artemeon\HttpClient\Exception\Request\Http;
/**
- * Exception class to catch all redirection related http errors (300 range)
+ * Exception class to catch all redirection related http errors (300 range).
*/
class RedirectResponseException extends ResponseException
{
-}
\ No newline at end of file
+}
diff --git a/src/Exception/Request/Http/ResponseException.php b/src/Exception/Request/Http/ResponseException.php
index 60fea1a..70ef524 100644
--- a/src/Exception/Request/Http/ResponseException.php
+++ b/src/Exception/Request/Http/ResponseException.php
@@ -17,30 +17,28 @@
use Artemeon\HttpClient\Http\Request;
use Artemeon\HttpClient\Http\Response;
use Exception;
-use Throwable;
/**
- * Exception class to catch all possible http status code ranges
+ * Exception class to catch all possible http status code ranges.
*/
class ResponseException extends TransferException
{
- protected ?Response $response;
+ protected ?Response $response = null;
protected int $statusCode;
/**
- * Named constructor to create an instance based on the response of the failed request
+ * Named constructor to create an instance based on the response of the failed request.
*
* @param ?Response $response The failed response if exists
* @param Request $request The failed request
* @param string $message The error message
* @param Exception|null $previous The previous exception
- * @return ResponseException
*/
public static function fromResponse(
?Response $response,
Request $request,
string $message,
- Exception $previous = null
+ ?Exception $previous = null,
): static {
$instance = new static($message, 0, $previous);
$instance->request = $request;
@@ -51,7 +49,7 @@ public static function fromResponse(
}
/**
- * Returns the Response object
+ * Returns the Response object.
*/
public function getResponse(): ?Response
{
@@ -59,7 +57,7 @@ public function getResponse(): ?Response
}
/**
- * Checks if we have a response object
+ * Checks if we have a response object.
*/
public function hasResponse(): bool
{
@@ -67,7 +65,7 @@ public function hasResponse(): bool
}
/**
- * Returns the http status code
+ * Returns the http status code.
*/
public function getStatusCode(): int
{
diff --git a/src/Exception/Request/Http/ServerResponseException.php b/src/Exception/Request/Http/ServerResponseException.php
index 7148f69..3c44d63 100644
--- a/src/Exception/Request/Http/ServerResponseException.php
+++ b/src/Exception/Request/Http/ServerResponseException.php
@@ -14,8 +14,8 @@
namespace Artemeon\HttpClient\Exception\Request\Http;
/**
- * Exception class to catch all server related http errors (500 range)
+ * Exception class to catch all server related http errors (500 range).
*/
class ServerResponseException extends ResponseException
{
-}
\ No newline at end of file
+}
diff --git a/src/Exception/Request/Network/ConnectException.php b/src/Exception/Request/Network/ConnectException.php
index 4c462f4..5f86dae 100644
--- a/src/Exception/Request/Network/ConnectException.php
+++ b/src/Exception/Request/Network/ConnectException.php
@@ -16,8 +16,8 @@
use Artemeon\HttpClient\Exception\Request\TransferException;
/**
- * Exception class to catch all network related exceptions
+ * Exception class to catch all network related exceptions.
*/
class ConnectException extends TransferException
{
-}
\ No newline at end of file
+}
diff --git a/src/Exception/Request/TransferException.php b/src/Exception/Request/TransferException.php
index 5eebfe7..4a21f15 100644
--- a/src/Exception/Request/TransferException.php
+++ b/src/Exception/Request/TransferException.php
@@ -19,20 +19,20 @@
use Throwable;
/**
- * Class for all runtime exceptions during the request/response transfers
+ * Class for all runtime exceptions during the request/response transfers.
*/
class TransferException extends RuntimeException
{
protected Request $request;
/**
- * Named constructor to create an instance based on the given request object
+ * Named constructor to create an instance based on the given request object.
*
* @param Request $request The failed request object
* @param string $message The error message
* @param Exception|null $previous The precious third party exception
*/
- public static function fromRequest(Request $request, string $message, Exception $previous = null): static
+ public static function fromRequest(Request $request, string $message, ?Exception $previous = null): static
{
$instance = new static($message, 0, $previous);
$instance->request = $request;
@@ -46,7 +46,7 @@ final public function __construct(string $message = '', int $code = 0, ?Throwabl
}
/**
- * Returns the request object of the failed request
+ * Returns the request object of the failed request.
*/
public function getRequest(): Request
{
diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php
index 20bdbb7..633b48c 100644
--- a/src/Exception/RuntimeException.php
+++ b/src/Exception/RuntimeException.php
@@ -17,7 +17,7 @@
use GuzzleHttp\Exception\GuzzleException;
/**
- * Base class to catch all possible runtime exceptions
+ * Base class to catch all possible runtime exceptions.
*
* ```
* 1. RuntimeException (All possible exceptions inclusive during instantiation)
diff --git a/src/Http/Body/Body.php b/src/Http/Body/Body.php
index 7e37bf9..24be9df 100644
--- a/src/Http/Body/Body.php
+++ b/src/Http/Body/Body.php
@@ -21,27 +21,23 @@
use Psr\Http\Message\StreamInterface;
/**
- * Value object to cover all http body related content
+ * Value object to cover all http body related content.
*/
class Body
{
- private int $length;
- private string $mimeType;
- private StreamInterface $stream;
+ private readonly int $length;
/**
* @param string $mimeType The mime type of the body content stream
* @param StreamInterface $stream The body content stream
*/
- private function __construct(string $mimeType, StreamInterface $stream)
+ private function __construct(private readonly string $mimeType, private readonly StreamInterface $stream)
{
- $this->mimeType = $mimeType;
- $this->stream = $stream;
- $this->length = $stream->getSize();
+ $this->length = $this->stream->getSize();
}
/**
- * Named constructor to create an instance based on the given values
+ * Named constructor to create an instance based on the given values.
*
* @param string $mimeType MIME-Type of the content
* @param string $value String to set the content
@@ -53,7 +49,7 @@ public static function fromString(string $mimeType, string $value): self
}
/**
- * Named constructor to create an instance based on the given Encoder
+ * Named constructor to create an instance based on the given Encoder.
*
* @param Encoder $encoder Body Encoder implementation
* @throws RuntimeException
@@ -64,9 +60,7 @@ public static function fromEncoder(Encoder $encoder): self
}
/**
- * Named constructor to create an instance based on the given Reader
- *
- * @param Reader $reader
+ * Named constructor to create an instance based on the given Reader.
*/
public static function fromReader(Reader $reader): self
{
@@ -77,7 +71,7 @@ public static function fromReader(Reader $reader): self
}
/**
- * Returns the calculated content length
+ * Returns the calculated content length.
*/
public function getContentLength(): int
{
@@ -85,7 +79,7 @@ public function getContentLength(): int
}
/**
- * Returns the associated mime type string
+ * Returns the associated mime type string.
*/
public function getMimeType(): string
{
@@ -93,7 +87,7 @@ public function getMimeType(): string
}
/**
- * Returns the content string
+ * Returns the content string.
*/
public function getStream(): StreamInterface
{
diff --git a/src/Http/Body/Encoder/Encoder.php b/src/Http/Body/Encoder/Encoder.php
index 1857973..221f774 100644
--- a/src/Http/Body/Encoder/Encoder.php
+++ b/src/Http/Body/Encoder/Encoder.php
@@ -17,19 +17,19 @@
use Psr\Http\Message\StreamInterface;
/**
- * Interface for http body Encoder
+ * Interface for http body Encoder.
*/
interface Encoder
{
/**
- * Encodes the body content
+ * Encodes the body content.
*
* @throws RuntimeException
*/
public function encode(): StreamInterface;
/**
- * Returns the supported MimeType
+ * Returns the supported MimeType.
*/
public function getMimeType(): string;
}
diff --git a/src/Http/Body/Encoder/FormUrlEncoder.php b/src/Http/Body/Encoder/FormUrlEncoder.php
index c8481d2..c92c6c7 100644
--- a/src/Http/Body/Encoder/FormUrlEncoder.php
+++ b/src/Http/Body/Encoder/FormUrlEncoder.php
@@ -15,27 +15,25 @@
use Artemeon\HttpClient\Http\MediaType;
use Artemeon\HttpClient\Stream\Stream;
+use Override;
use Psr\Http\Message\StreamInterface;
/**
- * Encoder for "application/x-www-form-urlencoded" encoded body content
+ * Encoder for "application/x-www-form-urlencoded" encoded body content.
*/
class FormUrlEncoder implements Encoder
{
- private array $formValues;
-
/**
* FormUrlEncoder constructor.
*
* @param array $formValues Array with the form values to encode: ['formFieldName' = 'value'],
*/
- private function __construct(array $formValues)
+ private function __construct(private readonly array $formValues)
{
- $this->formValues = $formValues;
}
/**
- * Named constructor to create an instance based on the given array
+ * Named constructor to create an instance based on the given array.
*
* ```php
* $encoder = FormUrlEncoder->fromArray(['username' = 'John.Doe'])
@@ -52,6 +50,7 @@ public static function fromArray(array $formValues): self
/**
* @inheritDoc
*/
+ #[Override]
public function encode(): StreamInterface
{
return Stream::fromString(http_build_query($this->formValues));
@@ -60,6 +59,7 @@ public function encode(): StreamInterface
/**
* @inheritDoc
*/
+ #[Override]
public function getMimeType(): string
{
return MediaType::FORM_URL_ENCODED;
diff --git a/src/Http/Body/Encoder/JsonEncoder.php b/src/Http/Body/Encoder/JsonEncoder.php
index bf20733..f5feb63 100644
--- a/src/Http/Body/Encoder/JsonEncoder.php
+++ b/src/Http/Body/Encoder/JsonEncoder.php
@@ -16,31 +16,27 @@
use Artemeon\HttpClient\Exception\RuntimeException;
use Artemeon\HttpClient\Http\MediaType;
use Artemeon\HttpClient\Stream\Stream;
+use Override;
use Psr\Http\Message\StreamInterface;
/**
- * Encoder for "application/json" encoded body content
+ * Encoder for "application/json" encoded body content.
*/
class JsonEncoder implements Encoder
{
- private array|object $value;
- private int $options;
- private string $mimeType;
-
/**
- * @param mixed $value String, object or array to encode
* @param int $options Optional json encode options: @see https://www.php.net/manual/de/function.json-encode.php
* @param string $mimeType Optional custom mime type
*/
- private function __construct(mixed $value, int $options = 0, string $mimeType = MediaType::JSON)
- {
- $this->value = $value;
- $this->options = $options;
- $this->mimeType = $mimeType;
+ private function __construct(
+ private readonly array | object $value,
+ private readonly int $options = 0,
+ private readonly string $mimeType = MediaType::JSON,
+ ) {
}
/**
- * Named constructor to create an instance based on the given array
+ * Named constructor to create an instance based on the given array.
*
* ```php
* # Associative arrays are always encoded as json object:
@@ -65,7 +61,7 @@ public static function fromArray(array $value, int $options = 0, string $mimeTyp
}
/**
- * Named constructor to create an instance based on the given object
+ * Named constructor to create an instance based on the given object.
*
* @param object $value Object to encode
* @param int $options Bitmask of json constants:
@@ -80,12 +76,14 @@ public static function fromObject(object $value, int $options = 0, string $mimeT
* @inheritDoc
* @throws RuntimeException
*/
+ #[Override]
public function encode(): StreamInterface
{
$json = json_encode($this->value, $this->options);
if ($json === false) {
$error = json_last_error_msg();
+
throw new RuntimeException("Can't encode to json: $error");
}
@@ -95,6 +93,7 @@ public function encode(): StreamInterface
/**
* @inheritDoc
*/
+ #[Override]
public function getMimeType(): string
{
return $this->mimeType;
diff --git a/src/Http/Body/Encoder/MultipartFormDataEncoder.php b/src/Http/Body/Encoder/MultipartFormDataEncoder.php
index d52b3e6..e8420b4 100644
--- a/src/Http/Body/Encoder/MultipartFormDataEncoder.php
+++ b/src/Http/Body/Encoder/MultipartFormDataEncoder.php
@@ -17,16 +17,17 @@
use Artemeon\HttpClient\Http\MediaType;
use Artemeon\HttpClient\Stream\AppendableStream;
use Artemeon\HttpClient\Stream\Stream;
+use Override;
use Psr\Http\Message\StreamInterface;
/**
- * Encoder for "multipart/form-data" encoded body content
+ * Encoder for "multipart/form-data" encoded body content.
*/
class MultipartFormDataEncoder implements Encoder
{
- private const CRLF = "\r\n";
- private string $boundary;
- private AppendableStream $multiPartStream;
+ private const string CRLF = "\r\n";
+ private readonly string $boundary;
+ private readonly AppendableStream $multiPartStream;
/**
* @param string $boundary Boundary string 7bit US-ASCII
@@ -39,18 +40,19 @@ private function __construct(string $boundary)
}
/**
- * Named constructor to create an instance
+ * Named constructor to create an instance.
*
* @throws RuntimeException
*/
public static function create(): self
{
$boundary = uniqid('');
+
return new self($boundary);
}
/**
- * Add a new multipart section for form fields
+ * Add a new multipart section for form fields.
*
* @param string $fieldName Name of the form field
* @param string $value Value of the form field
@@ -72,7 +74,7 @@ public function addFieldPart(string $fieldName, string $value): self
}
/**
- * Add a new multipart section for file upload fields
+ * Add a new multipart section for file upload fields.
*
* @param string $name Name of the form field
* @param string $fileName Name of the file, with a valid file extension
@@ -98,6 +100,7 @@ public function addFilePart(string $name, string $fileName, AppendableStream $fi
/**
* @inheritDoc
*/
+ #[Override]
public function encode(): StreamInterface
{
// Add the end boundary
@@ -109,15 +112,15 @@ public function encode(): StreamInterface
/**
* @inheritDoc
*/
+ #[Override]
public function getMimeType(): string
{
return sprintf('%s; boundary="%s"', MediaType::MULTIPART_FORM_DATA, $this->boundary);
}
/**
- * Detects the encoding of the given string
+ * Detects the encoding of the given string.
*
- * @param string $value
* @throws RuntimeException
*/
private function detectEncoding(string $value): string
diff --git a/src/Http/Body/Reader/FileReader.php b/src/Http/Body/Reader/FileReader.php
index 7672b96..ceb0927 100644
--- a/src/Http/Body/Reader/FileReader.php
+++ b/src/Http/Body/Reader/FileReader.php
@@ -15,33 +15,32 @@
use Artemeon\HttpClient\Exception\RuntimeException;
use Artemeon\HttpClient\Stream\Stream;
+use Override;
use Psr\Http\Message\StreamInterface;
/**
- * Reader to read body content from local and remote file system
+ * Reader to read body content from local and remote file system.
*/
class FileReader implements Reader
{
- private StreamInterface $stream;
- private string $file;
+ private readonly StreamInterface $stream;
/**
* @param StreamInterface $stream The content stream
* @param string $file The file path with file extension
* @throws RuntimeException
*/
- public function __construct(StreamInterface $stream, string $file)
+ public function __construct(StreamInterface $stream, private readonly string $file)
{
if (!$stream->isReadable()) {
throw new RuntimeException('Stream is nor readable');
}
$this->stream = $stream;
- $this->file = $file;
}
/**
- * Named construct to create an instance based on the given file path string
+ * Named construct to create an instance based on the given file path string.
*
* @param string $file Filename inclusive path and extension
* @throws RuntimeException
@@ -54,6 +53,7 @@ public static function fromFile(string $file): self
/**
* @inheritDoc
*/
+ #[Override]
public function getStream(): StreamInterface
{
return $this->stream;
@@ -62,6 +62,7 @@ public function getStream(): StreamInterface
/**
* @inheritDoc
*/
+ #[Override]
public function getFileExtension(): string
{
if (!preg_match("/\.([a-zA-Z]+)$/", $this->file, $matches)) {
diff --git a/src/Http/Body/Reader/Reader.php b/src/Http/Body/Reader/Reader.php
index aeb11ac..33887f4 100644
--- a/src/Http/Body/Reader/Reader.php
+++ b/src/Http/Body/Reader/Reader.php
@@ -16,17 +16,17 @@
use Psr\Http\Message\StreamInterface;
/**
- * Reader interface for body content
+ * Reader interface for body content.
*/
interface Reader
{
/**
- * Reads the body content as a Stream
+ * Reads the body content as a Stream.
*/
public function getStream(): StreamInterface;
/**
- * Returns the file extension of the read file
+ * Returns the file extension of the read file.
*/
public function getFileExtension(): string;
}
diff --git a/src/Http/Header/Fields/Authorization.php b/src/Http/Header/Fields/Authorization.php
index 1b6396b..ff119bf 100644
--- a/src/Http/Header/Fields/Authorization.php
+++ b/src/Http/Header/Fields/Authorization.php
@@ -14,9 +14,10 @@
namespace Artemeon\HttpClient\Http\Header\Fields;
use Artemeon\HttpClient\Http\Header\HeaderField;
+use Override;
/**
- * Class to describe the header field 'Authorisation'
+ * Class to describe the header field 'Authorisation'.
*
* Example:
* ```php
@@ -25,21 +26,16 @@
*/
class Authorization implements HeaderField
{
- private string $type;
- private string $credentials;
-
/**
* @param string $type The type of the http authorization
* @param string $credentials The credentials string
*/
- private function __construct(string $type, string $credentials)
+ private function __construct(private readonly string $type, private readonly string $credentials)
{
- $this->type = $type;
- $this->credentials = $credentials;
}
/**
- * Name constructor to create an 'Authorisation: Bearer' field
+ * Name constructor to create an 'Authorisation: Bearer' field.
*
* @param string $credentials String with credentials for Bearer authorisation
*/
@@ -49,7 +45,7 @@ public static function forAuthBearer(string $credentials): self
}
/**
- * Name constructor to create an 'Authorisation: Basic' field
+ * Name constructor to create an 'Authorisation: Basic' field.
*
* @param string $user String for the username
* @param string $password String for the password
@@ -62,6 +58,7 @@ public static function forAuthBasic(string $user, string $password): self
/**
* @inheritDoc
*/
+ #[Override]
public function getName(): string
{
return self::AUTHORIZATION;
@@ -70,6 +67,7 @@ public function getName(): string
/**
* @inheritDoc
*/
+ #[Override]
public function getValue(): string
{
return $this->type . ' ' . $this->credentials;
diff --git a/src/Http/Header/Fields/ContentLength.php b/src/Http/Header/Fields/ContentLength.php
index 01f1ed7..6ecb063 100644
--- a/src/Http/Header/Fields/ContentLength.php
+++ b/src/Http/Header/Fields/ContentLength.php
@@ -14,26 +14,19 @@
namespace Artemeon\HttpClient\Http\Header\Fields;
use Artemeon\HttpClient\Http\Header\HeaderField;
+use Override;
/**
- * Class to describe the header field 'Content-Length'
+ * Class to describe the header field 'Content-Length'.
*/
class ContentLength implements HeaderField
{
- private int $contentLength;
-
- /**
- * @param int $contentLength
- */
- public function __construct(int $contentLength)
+ public function __construct(private readonly int $contentLength)
{
- $this->contentLength = $contentLength;
}
/**
- * Named constructor to create an instance from the given int value
- *
- * @param int $contentLength
+ * Named constructor to create an instance from the given int value.
*/
public static function fromInt(int $contentLength): self
{
@@ -43,6 +36,7 @@ public static function fromInt(int $contentLength): self
/**
* @inheritDoc
*/
+ #[Override]
public function getName(): string
{
return HeaderField::CONTENT_LENGTH;
@@ -51,8 +45,9 @@ public function getName(): string
/**
* @inheritDoc
*/
+ #[Override]
public function getValue(): string
{
- return strval($this->contentLength);
+ return (string) ($this->contentLength);
}
}
diff --git a/src/Http/Header/Fields/ContentType.php b/src/Http/Header/Fields/ContentType.php
index 4a8fecf..31ac31a 100644
--- a/src/Http/Header/Fields/ContentType.php
+++ b/src/Http/Header/Fields/ContentType.php
@@ -14,24 +14,22 @@
namespace Artemeon\HttpClient\Http\Header\Fields;
use Artemeon\HttpClient\Http\Header\HeaderField;
+use Override;
/**
- * Class to describe the header field 'Content-Type'
+ * Class to describe the header field 'Content-Type'.
*/
class ContentType implements HeaderField
{
- private string $mimeType;
-
/**
* @param string $mimeType Mime type string
*/
- private function __construct(string $mimeType)
+ private function __construct(private readonly string $mimeType)
{
- $this->mimeType = $mimeType;
}
/**
- * Named constructor to create an instance from the given string value
+ * Named constructor to create an instance from the given string value.
*
* @param string $mimeType MIME type string @see \Artemeon\HttpClient\Http\MediaType
*/
@@ -43,6 +41,7 @@ public static function fromString(string $mimeType): self
/**
* @inheritDoc
*/
+ #[Override]
public function getName(): string
{
return HeaderField::CONTENT_TYPE;
@@ -51,6 +50,7 @@ public function getName(): string
/**
* @inheritDoc
*/
+ #[Override]
public function getValue(): string
{
return $this->mimeType;
diff --git a/src/Http/Header/Fields/Host.php b/src/Http/Header/Fields/Host.php
index 485ac46..93a4bf3 100644
--- a/src/Http/Header/Fields/Host.php
+++ b/src/Http/Header/Fields/Host.php
@@ -14,27 +14,23 @@
namespace Artemeon\HttpClient\Http\Header\Fields;
use Artemeon\HttpClient\Http\Header\HeaderField;
+use Override;
use Psr\Http\Message\UriInterface;
/**
- * Class to describe the header field 'Host'
+ * Class to describe the header field 'Host'.
*/
class Host implements HeaderField
{
- private string $host;
-
/**
* @param string $host The host string
*/
- private function __construct(string $host)
+ private function __construct(private readonly string $host)
{
- $this->host = $host;
}
/**
- * Named constructor to create an instance based on the given Url
- *
- * @param UriInterface $uri
+ * Named constructor to create an instance based on the given Url.
*/
public static function fromUri(UriInterface $uri): self
{
@@ -48,6 +44,7 @@ public static function fromUri(UriInterface $uri): self
/**
* @inheritDoc
*/
+ #[Override]
public function getName(): string
{
return HeaderField::HOST;
@@ -56,6 +53,7 @@ public function getName(): string
/**
* @inheritDoc
*/
+ #[Override]
public function getValue(): string
{
return $this->host;
diff --git a/src/Http/Header/Fields/UserAgent.php b/src/Http/Header/Fields/UserAgent.php
index 670fdca..d44ca5c 100644
--- a/src/Http/Header/Fields/UserAgent.php
+++ b/src/Http/Header/Fields/UserAgent.php
@@ -14,25 +14,24 @@
namespace Artemeon\HttpClient\Http\Header\Fields;
use Artemeon\HttpClient\Http\Header\HeaderField;
+use Override;
/**
- * Class to describe the header field 'User-Agent'
+ * Class to describe the header field 'User-Agent'.
*/
class UserAgent implements HeaderField
{
- public const DEFAULT = "Artemeon/HttpClient/Guzzle6";
- private string $userAgent;
+ public const string DEFAULT = 'Artemeon/HttpClient/Guzzle7';
/**
* @param string $userAgent The user agent string
*/
- public function __construct(string $userAgent)
+ public function __construct(private readonly string $userAgent)
{
- $this->userAgent = $userAgent;
}
/**
- * Named constructor to create an instance based on the given user agent string
+ * Named constructor to create an instance based on the given user agent string.
*
* @param string $userAgent User agent string
*/
@@ -44,6 +43,7 @@ public static function fromString(string $userAgent = self::DEFAULT): self
/**
* @inheritDoc
*/
+ #[Override]
public function getName(): string
{
return HeaderField::USER_AGENT;
@@ -52,6 +52,7 @@ public function getName(): string
/**
* @inheritDoc
*/
+ #[Override]
public function getValue(): string
{
return $this->userAgent;
diff --git a/src/Http/Header/Header.php b/src/Http/Header/Header.php
index dbd2a71..ab59b43 100644
--- a/src/Http/Header/Header.php
+++ b/src/Http/Header/Header.php
@@ -16,11 +16,11 @@
use Artemeon\HttpClient\Exception\InvalidArgumentException;
/**
- * Value object for parsed http header fields
+ * Value object for parsed http header fields.
*/
class Header
{
- private string $name;
+ private readonly string $name;
private array $values;
/**
@@ -35,7 +35,7 @@ private function __construct(string $name, array $values)
}
/**
- * Named constructor to create an instance based on the given string value
+ * Named constructor to create an instance based on the given string value.
*
* @param string $name Name of the http header field
* @param string $value Value of the http header field
@@ -47,7 +47,7 @@ public static function fromString(string $name, string $value): self
}
/**
- * Named constructor to create an instance based on the given string[] values
+ * Named constructor to create an instance based on the given string[] values.
*
* @param string $name Name of the http header field
* @param array $values Array of header values
@@ -59,9 +59,8 @@ public static function fromArray(string $name, array $values): self
}
/**
- * Named constructor to create an instance based on the HeaderField object
+ * Named constructor to create an instance based on the HeaderField object.
*
- * @param HeaderField $headerField
* @throws InvalidArgumentException
*/
public static function fromField(HeaderField $headerField): self
@@ -70,7 +69,7 @@ public static function fromField(HeaderField $headerField): self
}
/**
- * Return the http header field name like "Accept-Encoding"
+ * Return the http header field name like "Accept-Encoding".
*/
public function getFieldName(): string
{
@@ -78,7 +77,7 @@ public function getFieldName(): string
}
/**
- * Add a value to the header
+ * Add a value to the header.
*
* @param string $value The string value to add
*/
@@ -88,7 +87,7 @@ public function addValue(string $value): void
}
/**
- * Add an array of values to the header, doublets will be skipped
+ * Add an array of values to the header, doublets will be skipped.
*
* @param array $values The string value to add
*/
@@ -105,7 +104,7 @@ public function addValues(array $values): void
}
/**
- * Returns all value of the http header field
+ * Returns all value of the http header field.
*/
public function getValues(): array
{
@@ -113,7 +112,7 @@ public function getValues(): array
}
/**
- * Returns all values as a concatenated comma separated string
+ * Returns all values as a concatenated comma separated string.
*/
public function getValue(): string
{
@@ -121,9 +120,8 @@ public function getValue(): string
}
/**
- * Check and normalize header values
+ * Check and normalize header values.
*
- * @param array $values
* @throws InvalidArgumentException
*/
private function assertValues(array $values): array
@@ -133,11 +131,10 @@ private function assertValues(array $values): array
}
foreach ($values as &$value) {
- $value = trim($value);
- $isInvalidValue = !is_numeric($value) && !is_string($value);
- $containsInvalidCharacters = preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string)$value) !== 1;
+ $value = trim((string) $value);
+ $containsInvalidCharacters = preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", $value) !== 1;
- if ($isInvalidValue || $containsInvalidCharacters) {
+ if ($containsInvalidCharacters) {
throw new InvalidArgumentException('Header values must be RFC 7230 compatible strings.');
}
}
@@ -146,16 +143,15 @@ private function assertValues(array $values): array
}
/**
- * Check vor valid header name
+ * Check vor valid header name.
*
- * @param string $name
* @throws InvalidArgumentException
*/
private function assertName(string $name): string
{
$name = trim($name);
- if (1 !== preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $name)) {
+ if (preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $name) !== 1) {
throw new InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
}
diff --git a/src/Http/Header/HeaderField.php b/src/Http/Header/HeaderField.php
index aae4551..b319f58 100644
--- a/src/Http/Header/HeaderField.php
+++ b/src/Http/Header/HeaderField.php
@@ -14,24 +14,24 @@
namespace Artemeon\HttpClient\Http\Header;
/**
- * Interface for http header fields
+ * Interface for http header fields.
*/
interface HeaderField
{
- public const AUTHORIZATION = 'Authorization';
- public const REFERER = "Referer";
- public const USER_AGENT = "User-Agent";
- public const CONTENT_TYPE = 'Content-Type';
- public const CONTENT_LENGTH = 'Content-Length';
- public const HOST = 'Host';
+ public const string AUTHORIZATION = 'Authorization';
+ public const string REFERER = 'Referer';
+ public const string USER_AGENT = 'User-Agent';
+ public const string CONTENT_TYPE = 'Content-Type';
+ public const string CONTENT_LENGTH = 'Content-Length';
+ public const string HOST = 'Host';
/**
- * Returns the name of the field
+ * Returns the name of the field.
*/
public function getName(): string;
/**
- * Returns the value of the field
+ * Returns the value of the field.
*/
public function getValue(): string;
}
diff --git a/src/Http/Header/Headers.php b/src/Http/Header/Headers.php
index 607a9bc..5f0f971 100644
--- a/src/Http/Header/Headers.php
+++ b/src/Http/Header/Headers.php
@@ -17,9 +17,10 @@
use Artemeon\HttpClient\Exception\InvalidArgumentException;
use Countable;
use IteratorAggregate;
+use Override;
/**
- * Header collection class for http requests and responses
+ * Header collection class for http requests and responses.
*/
class Headers implements Countable, IteratorAggregate
{
@@ -27,7 +28,7 @@ class Headers implements Countable, IteratorAggregate
private array $headers = [];
/**
- * Named constructor to create an instance based on the given array of HeaderField objects
+ * Named constructor to create an instance based on the given array of HeaderField objects.
*
* @param HeaderField[] $headerFields
* @throws InvalidArgumentException
@@ -44,7 +45,7 @@ public static function fromFields(array $headerFields): self
}
/**
- * Named constructor to create an empty collection instance
+ * Named constructor to create an empty collection instance.
*/
public static function create(): self
{
@@ -52,7 +53,7 @@ public static function create(): self
}
/**
- * Adds a header to the collection, throws an exception if the header already exists
+ * Adds a header to the collection, throws an exception if the header already exists.
*
* @param Header $header The Header to add
* @throws InvalidArgumentException
@@ -74,7 +75,7 @@ public function add(Header $header): void
}
/**
- * Adds a header to the collection or replaces an already existing header
+ * Adds a header to the collection or replaces an already existing header.
*
* @param Header $header The header to replace
*/
@@ -91,18 +92,19 @@ public function replace(Header $header): void
}
/**
- * Checks case incentive for a specific header field
+ * Checks case incentive for a specific header field.
*
* @param string $headerField The header field to check
*/
public function has(string $headerField): bool
{
$headerField = strtolower($headerField);
+
return isset($this->headers[$headerField]);
}
/**
- * Checks if the header with given headerField contains an empty value string
+ * Checks if the header with given headerField contains an empty value string.
*
* @param string $headerField The header field to check
*/
@@ -110,13 +112,13 @@ public function isEmpty(string $headerField): bool
{
try {
return empty($this->get($headerField)->getValue());
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
return true;
}
}
/**
- * Removes the header with the given header field name
+ * Removes the header with the given header field name.
*
* @param string $headerField The header field to remove
*/
@@ -132,7 +134,7 @@ public function remove(string $headerField): void
}
/**
- * Return a Header object for the given header field name
+ * Return a Header object for the given header field name.
*
* @param string $headerField The header to get
* @throws InvalidArgumentException
@@ -151,6 +153,7 @@ public function get(string $headerField): Header
/**
* @inheritDoc
*/
+ #[Override]
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->headers);
@@ -159,6 +162,7 @@ public function getIterator(): ArrayIterator
/**
* @inheritDoc
*/
+ #[Override]
public function count(): int
{
return count($this->headers);
diff --git a/src/Http/MediaType.php b/src/Http/MediaType.php
index bb3d484..ea48147 100644
--- a/src/Http/MediaType.php
+++ b/src/Http/MediaType.php
@@ -14,42 +14,21 @@
namespace Artemeon\HttpClient\Http;
/**
- * Static class to describe media type MIME types
+ * Static class to describe media type MIME types.
*/
class MediaType
{
- /** @var string */
- public const JSON = "application/json";
-
- /** @var string */
- public const JSON_API = "application/vnd.api+json";
-
- /** @var string */
- public const FORM_URL_ENCODED = "application/x-www-form-urlencoded";
-
- /** @var string */
- public const MULTIPART_FORM_DATA = "multipart/form-data";
-
- /** @var string */
- public const PDF = "application/pdf";
-
- /** @var string */
- public const XML = "application/xml";
-
- /** @var string */
- public const BMP = "image/x-ms-bmp";
-
- /** @var string */
- public const GIF = "image/gif";
-
- /** @var string */
- public const JPG = "image/jpeg";
-
- /** @var string */
- public const PNG = "image/png";
-
- /** @var string */
- public const UNKNOWN = "application/octet-stream";
+ public const string JSON = 'application/json';
+ public const string JSON_API = 'application/vnd.api+json';
+ public const string FORM_URL_ENCODED = 'application/x-www-form-urlencoded';
+ public const string MULTIPART_FORM_DATA = 'multipart/form-data';
+ public const string PDF = 'application/pdf';
+ public const string XML = 'application/xml';
+ public const string BMP = 'image/x-ms-bmp';
+ public const string GIF = 'image/gif';
+ public const string JPG = 'image/jpeg';
+ public const string PNG = 'image/png';
+ public const string UNKNOWN = 'application/octet-stream';
/** @var string[] */
private static array $extensionToType = [
@@ -63,7 +42,7 @@ class MediaType
];
/**
- * Static helper function to map a file extension to the related MIME type
+ * Static helper function to map a file extension to the related MIME type.
*
* @param string $fileExtension The file extension string
*/
@@ -71,10 +50,6 @@ public static function mapFileExtensionToMimeType(string $fileExtension): string
{
$fileExtension = strtolower($fileExtension);
- if (isset(self::$extensionToType[$fileExtension])) {
- return self::$extensionToType[$fileExtension];
- }
-
- return self::UNKNOWN;
+ return self::$extensionToType[$fileExtension] ?? self::UNKNOWN;
}
}
diff --git a/src/Http/Message.php b/src/Http/Message.php
index 5c48c7c..e3eeb19 100644
--- a/src/Http/Message.php
+++ b/src/Http/Message.php
@@ -18,35 +18,33 @@
use Artemeon\HttpClient\Http\Header\Header;
use Artemeon\HttpClient\Http\Header\Headers;
use Artemeon\HttpClient\Stream\Stream;
+use Override;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\StreamInterface;
/**
- * Abstract class to describe a http message
+ * Abstract class to describe a http message.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages
*/
abstract class Message implements MessageInterface
{
protected Headers $headers;
- protected ?StreamInterface $body;
- protected string $version;
/**
* @param Headers|null $headers Optional: Headers collection or null
* @param StreamInterface|null $body Optional: Body object or null
* @param string $version Optional: Http protocol version string
*/
- protected function __construct(?Headers $headers = null, StreamInterface $body = null, string $version = '1.1')
+ protected function __construct(?Headers $headers = null, protected ?StreamInterface $body = null, protected string $version = '1.1')
{
$this->headers = $headers ?? Headers::create();
- $this->body = $body;
- $this->version = $version;
}
/**
- * Return the Header collection as an array
+ * Return the Header collection as an array.
*/
+ #[Override]
public function getHeaders(): array
{
$headers = [];
@@ -61,8 +59,10 @@ public function getHeaders(): array
/**
* @inheritDoc
+ *
* @throws RuntimeException
*/
+ #[Override]
public function getBody(): StreamInterface
{
if (!$this->body instanceof StreamInterface) {
@@ -75,6 +75,7 @@ public function getBody(): StreamInterface
/**
* @inheritDoc
*/
+ #[Override]
public function getProtocolVersion(): string
{
return $this->version;
@@ -83,19 +84,21 @@ public function getProtocolVersion(): string
/**
* @inheritDoc
*/
- public function hasHeader($name): bool
+ #[Override]
+ public function hasHeader(string $name): bool
{
- return $this->headers->has(strval($name));
+ return $this->headers->has($name);
}
/**
* @inheritDoc
*/
- public function getHeader($name): array
+ #[Override]
+ public function getHeader(string $name): array
{
try {
- return $this->headers->get(strval($name))->getValues();
- } catch (InvalidArgumentException $e) {
+ return $this->headers->get($name)->getValues();
+ } catch (InvalidArgumentException) {
return [];
}
}
@@ -103,11 +106,12 @@ public function getHeader($name): array
/**
* @inheritDoc
*/
- public function getHeaderLine($name): string
+ #[Override]
+ public function getHeaderLine(string $name): string
{
try {
- return $this->headers->get(strval($name))->getValue();
- } catch (InvalidArgumentException $e) {
+ return $this->headers->get($name)->getValue();
+ } catch (InvalidArgumentException) {
return '';
}
}
@@ -115,7 +119,8 @@ public function getHeaderLine($name): string
/**
* @inheritDoc
*/
- public function withHeader($name, $value): self
+ #[Override]
+ public function withHeader(string $name, $value): self
{
$cloned = clone $this;
$cloned->assertHeader($name, $value);
@@ -132,10 +137,11 @@ public function withHeader($name, $value): self
/**
* @inheritDoc
*/
- public function withProtocolVersion($version): self
+ #[Override]
+ public function withProtocolVersion(string $version): self
{
$cloned = clone $this;
- $cloned->version = strval($version);
+ $cloned->version = $version;
return $cloned;
}
@@ -143,7 +149,8 @@ public function withProtocolVersion($version): self
/**
* @inheritDoc
*/
- public function withAddedHeader($name, $value): self
+ #[Override]
+ public function withAddedHeader(string $name, $value): self
{
$cloned = clone $this;
$cloned->assertHeader($name, $value);
@@ -155,7 +162,7 @@ public function withAddedHeader($name, $value): self
$cloned->headers->get($name)->addValue($value);
}
} else {
- // Field does not exists, create new header
+ // Field does not exist, create new header
$header = is_array($value) ? Header::fromArray($name, $value) : Header::fromString($name, $value);
$cloned->headers->add($header);
}
@@ -166,7 +173,8 @@ public function withAddedHeader($name, $value): self
/**
* @inheritDoc
*/
- public function withoutHeader($name)
+ #[Override]
+ public function withoutHeader(string $name): MessageInterface
{
$cloned = clone $this;
$cloned->headers->remove($name);
@@ -177,7 +185,8 @@ public function withoutHeader($name)
/**
* @inheritDoc
*/
- public function withBody(StreamInterface $body)
+ #[Override]
+ public function withBody(StreamInterface $body): MessageInterface
{
if (!$body->isReadable()) {
throw new InvalidArgumentException('Body stream must be readable');
@@ -190,15 +199,13 @@ public function withBody(StreamInterface $body)
}
/**
- * Checks the header data
+ * Checks the header data.
*
- * @param $name
- * @param $value
* @throws InvalidArgumentException
*/
- private function assertHeader($name, $value): void
+ private function assertHeader(string $name, array | float | int | string $value): void
{
- if (!is_string($name) || $name === '') {
+ if ($name === '') {
throw new InvalidArgumentException('Header must be a non empty string');
}
@@ -208,8 +215,6 @@ private function assertHeader($name, $value): void
throw new InvalidArgumentException('Values must a string or numeric');
}
}
- } elseif (!is_string($value) && !is_numeric($value)) {
- throw new InvalidArgumentException('Values must a string or numeric');
}
}
}
diff --git a/src/Http/Request.php b/src/Http/Request.php
index 4dcfc9e..e51041c 100644
--- a/src/Http/Request.php
+++ b/src/Http/Request.php
@@ -21,60 +21,55 @@
use Artemeon\HttpClient\Http\Header\Header;
use Artemeon\HttpClient\Http\Header\HeaderField;
use Artemeon\HttpClient\Http\Header\Headers;
+use Override;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
/**
- * Implementation of the psr7 RequestInterface
+ * Implementation of the psr7 RequestInterface.
*/
class Request extends Message implements RequestInterface
{
- public const METHOD_POST = 'POST';
- public const METHOD_GET = 'GET';
- public const METHOD_PUT = 'PUT';
- public const METHOD_DELETE = 'DELETE';
- public const METHOD_OPTIONS = 'OPTIONS';
- public const METHOD_PATCH = 'PATCH';
- public const METHOD_HEAD = 'HEAD';
+ public const string METHOD_POST = 'POST';
+ public const string METHOD_GET = 'GET';
+ public const string METHOD_PUT = 'PUT';
+ public const string METHOD_DELETE = 'DELETE';
+ public const string METHOD_OPTIONS = 'OPTIONS';
+ public const string METHOD_PATCH = 'PATCH';
+ public const string METHOD_HEAD = 'HEAD';
private string $method;
- private UriInterface $uri;
- private string $requestTarget;
+
+ private ?string $requestTarget = null;
/**
- * @param string $method
- * @param UriInterface $uri
- * @param Headers|null $headers
- * @param StreamInterface|null $body
- * @param string $version
* @throws InvalidArgumentException
*/
private function __construct(
string $method,
- UriInterface $uri,
+ private UriInterface $uri,
?Headers $headers = null,
?StreamInterface $body = null,
- string $version = '1.1'
+ string $version = '1.1',
) {
- $this->uri = $uri;
- $this->requestTarget = $this->parseRequestTarget($uri);
$this->assertValidMethod($method);
$this->method = $method;
parent::__construct(
- $this->addHostHeader($uri, $headers),
+ $this->addHostHeader($this->uri, $headers),
$body,
- $version
+ $version,
);
}
/**
- * Named constructor to create an instance for post requests
+ * Named constructor to create an instance for GET requests.
*
* @param Uri $uri The Url object
* @param Headers|null $headers Optional: Headers collection or null
* @param string $version Optional: http protocol version string
+ *
* @throws InvalidArgumentException
*/
public static function forGet(Uri $uri, ?Headers $headers = null, string $version = '1.1'): self
@@ -84,16 +79,17 @@ public static function forGet(Uri $uri, ?Headers $headers = null, string $versio
$uri,
$headers,
null,
- $version
+ $version,
);
}
/**
- * Named constructor to create an instance for OPTIONS requests
+ * Named constructor to create an instance for OPTIONS requests.
*
* @param Uri $uri The Url object
* @param Headers|null $headers Optional: Headers collection or null
* @param string $version Optional: the http protocol version string
+ *
* @throws InvalidArgumentException
*/
public static function forOptions(Uri $uri, ?Headers $headers = null, string $version = '1.1'): self
@@ -103,17 +99,18 @@ public static function forOptions(Uri $uri, ?Headers $headers = null, string $ve
$uri,
$headers,
null,
- $version
+ $version,
);
}
/**
- * Named constructor to create an instance for POST requests
+ * Named constructor to create an instance for POST requests.
*
* @param Uri $uri The Url object
* @param Body $body The Body object
* @param Headers|null $headers Optional: Headers collection or null
* @param string $version Optional: the http protocol version string
+ *
* @throws InvalidArgumentException
*/
public static function forPost(Uri $uri, Body $body, ?Headers $headers = null, string $version = '1.1'): self
@@ -125,17 +122,18 @@ public static function forPost(Uri $uri, Body $body, ?Headers $headers = null, s
$uri,
$headers,
$body->getStream(),
- $version
+ $version,
);
}
/**
- * Named constructor to create an instance for PUT requests
+ * Named constructor to create an instance for PUT requests.
*
* @param Uri $uri The Url object
* @param Body $body The Body object
* @param Headers|null $headers Optional: Headers collection or null
* @param string $version Optional: the http protocol version string
+ *
* @throws InvalidArgumentException
*/
public static function forPut(Uri $uri, Body $body, ?Headers $headers = null, string $version = '1.1'): self
@@ -147,17 +145,18 @@ public static function forPut(Uri $uri, Body $body, ?Headers $headers = null, st
$uri,
$headers,
$body->getStream(),
- $version
+ $version,
);
}
/**
- * Named constructor to create an instance for PATCH requests
+ * Named constructor to create an instance for PATCH requests.
*
* @param Uri $uri The Url object
* @param Body $body The Body object
* @param Headers|null $headers Optional: Headers collection or null
* @param string $version Optional: the http protocol version string
+ *
* @throws InvalidArgumentException
*/
public static function forPatch(Uri $uri, Body $body, ?Headers $headers = null, string $version = '1.1'): self
@@ -169,16 +168,17 @@ public static function forPatch(Uri $uri, Body $body, ?Headers $headers = null,
$uri,
$headers,
$body->getStream(),
- $version
+ $version,
);
}
/**
- * Named constructor to create an instance for DELETE requests
+ * Named constructor to create an instance for DELETE requests.
*
* @param Uri $uri The Url object
* @param Headers|null $headers Optional: Headers collection or null
* @param string $version Optional: http protocol version string
+ *
* @throws InvalidArgumentException
*/
public static function forDelete(Uri $uri, ?Headers $headers = null, string $version = '1.1'): self
@@ -188,13 +188,14 @@ public static function forDelete(Uri $uri, ?Headers $headers = null, string $ver
$uri,
$headers,
null,
- $version
+ $version,
);
}
/**
* @inheritDoc
*/
+ #[Override]
public function getMethod(): string
{
return $this->method;
@@ -203,12 +204,9 @@ public function getMethod(): string
/**
* @inheritDoc
*/
- public function withMethod($method): self
+ #[Override]
+ public function withMethod(string $method): self
{
- if (!is_string($method)) {
- throw new InvalidArgumentException('method must be a string value');
- }
-
$this->assertValidMethod($method);
$cloned = clone $this;
@@ -220,6 +218,7 @@ public function withMethod($method): self
/**
* @inheritDoc
*/
+ #[Override]
public function getUri(): UriInterface
{
return $this->uri;
@@ -227,12 +226,19 @@ public function getUri(): UriInterface
/**
* @inheritDoc
+ *
* @throws InvalidArgumentException
*/
- public function withUri(UriInterface $uri, $preserveHost = false): self
+ #[Override]
+ public function withUri(UriInterface $uri, bool $preserveHost = false): self
{
+ if ($uri === $this->uri) {
+ return $this;
+ }
+
+ $normalizedPath = preg_replace('#^/+#', '/', $uri->getPath());
$cloned = clone $this;
- $cloned->uri = $uri;
+ $cloned->uri = $uri->withPath($normalizedPath);
$newHost = Header::fromString(HeaderField::HOST, $uri->getHost());
@@ -252,32 +258,42 @@ public function withUri(UriInterface $uri, $preserveHost = false): self
/**
* @inheritDoc
*/
+ #[Override]
public function getRequestTarget(): string
{
- return $this->requestTarget;
+ if ($this->requestTarget !== null) {
+ return $this->requestTarget;
+ }
+
+ return $this->parseRequestTarget($this->uri);
}
/**
* @inheritDoc
*/
- public function withRequestTarget($requestTarget): self
+ #[Override]
+ public function withRequestTarget(string $requestTarget): self
{
+ if (preg_match('#\s#', $requestTarget)) {
+ throw new InvalidArgumentException(
+ 'Invalid request target provided; cannot contain whitespace',
+ );
+ }
+
$cloned = clone $this;
- $cloned->requestTarget = trim(strval($requestTarget));
+ $cloned->requestTarget = trim((string) $requestTarget);
return $cloned;
}
/**
- * Add the calculated header fields from the body to the headers collection
+ * Add the calculated header fields from the body to the headers collection.
*
- * @param Body $body
- * @param Headers|null $headers
* @throws InvalidArgumentException
*/
private static function addHeaderFromBody(Body $body, ?Headers $headers): Headers
{
- $headers = $headers ?? Headers::create();
+ $headers ??= Headers::create();
$headers->add(Header::fromField(ContentType::fromString($body->getMimeType())));
$headers->add(Header::fromField(ContentLength::fromInt($body->getContentLength())));
@@ -285,10 +301,8 @@ private static function addHeaderFromBody(Body $body, ?Headers $headers): Header
}
/**
- * Add the host header based on the given Url
+ * Add the host header based on the given Url.
*
- * @param UriInterface $uri
- * @param Headers|null $headers
* @throws InvalidArgumentException
*/
private function addHostHeader(UriInterface $uri, ?Headers $headers): Headers
@@ -303,9 +317,8 @@ private function addHostHeader(UriInterface $uri, ?Headers $headers): Headers
}
/**
- * Checks for valid request methods
+ * Checks for valid request methods.
*
- * @param string $method
* @throws InvalidArgumentException
*/
private function assertValidMethod(string $method): void
@@ -326,10 +339,7 @@ private function assertValidMethod(string $method): void
}
/**
- * Parse the standard request target from the given Uri
- *
- * @param UriInterface $uri
- * @return string
+ * Parse the standard request target from the given Uri.
*/
private function parseRequestTarget(UriInterface $uri): string
{
diff --git a/src/Http/Response.php b/src/Http/Response.php
index bfc9a60..97fffab 100644
--- a/src/Http/Response.php
+++ b/src/Http/Response.php
@@ -13,19 +13,16 @@
namespace Artemeon\HttpClient\Http;
-use Artemeon\HttpClient\Exception\InvalidArgumentException;
use Artemeon\HttpClient\Http\Header\Headers;
+use Override;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
- * PSR-7 Response class
+ * PSR-7 Response class.
*/
class Response extends Message implements ResponseInterface
{
- private int $statusCode;
- private string $reasonPhrase;
-
/**
* @param int $statusCode The http status code
* @param string $version The http version number without http prefix
@@ -34,20 +31,19 @@ class Response extends Message implements ResponseInterface
* @param string $reasonPhrase The http response reason phrase
*/
public function __construct(
- int $statusCode,
+ private int $statusCode,
string $version,
- StreamInterface $body = null,
- Headers $headers = null,
- string $reasonPhrase = ''
+ ?StreamInterface $body = null,
+ ?Headers $headers = null,
+ private string $reasonPhrase = '',
) {
- $this->statusCode = $statusCode;
- $this->reasonPhrase = $reasonPhrase;
parent::__construct($headers, $body, $version);
}
/**
* @inheritDoc
*/
+ #[Override]
public function getStatusCode(): int
{
return $this->statusCode;
@@ -56,16 +52,9 @@ public function getStatusCode(): int
/**
* @inheritDoc
*/
- public function withStatus($code, $reasonPhrase = '')
+ #[Override]
+ public function withStatus(int $code, string $reasonPhrase = ''): ResponseInterface
{
- if (!is_string($reasonPhrase)) {
- throw new InvalidArgumentException('reasonPhrase must be a string value');
- }
-
- if (!is_int($code)) {
- throw new InvalidArgumentException('code must be a integer value');
- }
-
if ($code < 100 || $code >= 600) {
throw new \InvalidArgumentException('code must be an integer value between 100 and 599');
}
@@ -80,6 +69,7 @@ public function withStatus($code, $reasonPhrase = '')
/**
* @inheritDoc
*/
+ #[Override]
public function getReasonPhrase(): string
{
return $this->reasonPhrase;
diff --git a/src/Http/Uri.php b/src/Http/Uri.php
index da9b054..2822d15 100644
--- a/src/Http/Uri.php
+++ b/src/Http/Uri.php
@@ -14,10 +14,11 @@
namespace Artemeon\HttpClient\Http;
use Artemeon\HttpClient\Exception\InvalidArgumentException;
+use Override;
use Psr\Http\Message\UriInterface;
/**
- * Class Url implements the PSR-7 UriInterface
+ * Class Url implements the PSR-7 UriInterface.
*/
class Uri implements UriInterface
{
@@ -26,13 +27,13 @@ class Uri implements UriInterface
private string $host = '';
private string $user = '';
private string $password = '';
- private ?int $port;
+ private ?int $port = null;
private string $path = '';
private string $fragment = '';
- private const UNRESERVED = 'a-zA-Z0-9_\-\.~';
- private const DELIMITER = '!\$&\'\(\)\*\+,;=';
+ private const string UNRESERVED = 'a-zA-Z0-9_\-\.~';
+ private const string DELIMITER = '!\$&\'\(\)\*\+,;=';
- private const STANDARD_PORTS = [
+ private const array STANDARD_PORTS = [
'http' => 80,
'https' => 443,
'ftp' => 21,
@@ -52,20 +53,22 @@ class Uri implements UriInterface
*/
private function __construct(string $uri)
{
- if ($uri !== '') {
- $this->query = $this->filterQueryOrFragment(parse_url($uri, PHP_URL_QUERY) ?? '');
- $this->scheme = $this->filterScheme(parse_url($uri, PHP_URL_SCHEME) ?? '');
- $this->host = $this->filterHost(parse_url($uri, PHP_URL_HOST) ?? '');
- $this->port = $this->filterPort(parse_url($uri, PHP_URL_PORT) ?? null);
- $this->fragment = $this->filterQueryOrFragment(parse_url($uri, PHP_URL_FRAGMENT) ?? '');
- $this->path = $this->filterPath(parse_url($uri, PHP_URL_PATH) ?? '');
- $this->user = parse_url($uri, PHP_URL_USER) ?? '';
- $this->password = parse_url($uri, PHP_URL_PASS) ?? '';
+ if ($uri === '') {
+ return;
}
+
+ $this->query = $this->filterQueryOrFragment(parse_url($uri, PHP_URL_QUERY) ?? '');
+ $this->scheme = $this->filterScheme(parse_url($uri, PHP_URL_SCHEME) ?? '');
+ $this->host = $this->filterHost(parse_url($uri, PHP_URL_HOST) ?? '');
+ $this->port = $this->filterPort(parse_url($uri, PHP_URL_PORT) ?? null);
+ $this->fragment = $this->filterQueryOrFragment(parse_url($uri, PHP_URL_FRAGMENT) ?? '');
+ $this->path = $this->filterPath(parse_url($uri, PHP_URL_PATH) ?? '');
+ $this->user = parse_url($uri, PHP_URL_USER) ?? '';
+ $this->password = parse_url($uri, PHP_URL_PASS) ?? '';
}
/**
- * Named constructor to create an instance based on the given url and query params
+ * Named constructor to create an instance based on the given url and query params.
*
* @param string $uri Url string with protocol
* @param array $queryParams Query params array: ["varName" => value]
@@ -94,6 +97,7 @@ public static function fromString(string $uri): self
/**
* @inheritDoc
*/
+ #[Override]
public function getScheme(): string
{
return $this->scheme;
@@ -102,6 +106,7 @@ public function getScheme(): string
/**
* @inheritDoc
*/
+ #[Override]
public function getHost(): string
{
return $this->host;
@@ -110,6 +115,7 @@ public function getHost(): string
/**
* @inheritDoc
*/
+ #[Override]
public function getPort(): ?int
{
if ($this->isStandardPort($this->scheme, $this->port)) {
@@ -122,28 +128,33 @@ public function getPort(): ?int
/**
* @inheritDoc
*/
+ #[Override]
public function getUserInfo(): string
{
if ($this->user !== '') {
if ($this->password !== '') {
return $this->user . ':' . $this->password;
}
+
return $this->user;
}
+
return '';
}
/**
* @inheritDoc
*/
+ #[Override]
public function getPath(): string
{
- return $this->path;
+ return preg_replace('#^/+#', '/', $this->path);
}
/**
* @inheritDoc
*/
+ #[Override]
public function getQuery(): string
{
return $this->query;
@@ -152,6 +163,7 @@ public function getQuery(): string
/**
* @inheritDoc
*/
+ #[Override]
public function getFragment(): string
{
return $this->fragment;
@@ -160,6 +172,7 @@ public function getFragment(): string
/**
* @inheritDoc
*/
+ #[Override]
public function __toString(): string
{
$uri = ($this->getScheme() !== '') ? $this->getScheme() . ':' : '';
@@ -168,8 +181,9 @@ public function __toString(): string
$uri .= '//' . $this->getAuthority();
}
- if ($this->getPath() !== '') {
- $uri .= $this->getPath();
+ // not normalized like //valid/path
+ if ($this->path !== '') {
+ $uri .= $this->path;
}
if ($this->getQuery() !== '') {
@@ -186,6 +200,7 @@ public function __toString(): string
/**
* @inheritDoc
*/
+ #[Override]
public function getAuthority(): string
{
$authority = ($this->getPort() === null) ? $this->host : $this->host . ':' . $this->port;
@@ -200,7 +215,8 @@ public function getAuthority(): string
/**
* @inheritDoc
*/
- public function withScheme($scheme): self
+ #[Override]
+ public function withScheme(string $scheme): self
{
$this->filterScheme($scheme);
@@ -213,10 +229,14 @@ public function withScheme($scheme): self
/**
* @inheritDoc
*/
- public function withUserInfo($user, $password = null): self
+ #[Override]
+ public function withUserInfo(string $user, ?string $password = null): self
{
- $user = trim(strval($user));
- $password = trim(strval($password));
+ $user = $this->filterUserInfoComponent($user);
+ if ($password !== null) {
+ $password = $this->filterUserInfoComponent($password);
+ }
+
$cloned = clone $this;
// Empty string for the user is equivalent to removing user
@@ -225,16 +245,31 @@ public function withUserInfo($user, $password = null): self
$cloned->password = '';
} else {
$cloned->user = $user;
- $cloned->password = $password;
+ $cloned->password = $password ?? '';
}
return $cloned;
}
+ private function filterUserInfoComponent(string $component): string
+ {
+ return preg_replace_callback(
+ '/(?:[^%' . self::UNRESERVED . self::DELIMITER . ']+|%(?![A-Fa-f0-9]{2}))/',
+ [$this, 'rawurlencodeMatchZero'],
+ $component,
+ );
+ }
+
+ private function rawurlencodeMatchZero(array $match): string
+ {
+ return rawurlencode((string) $match[0]);
+ }
+
/**
* @inheritDoc
*/
- public function withHost($host): self
+ #[Override]
+ public function withHost(string $host): self
{
$cloned = clone $this;
$cloned->host = $cloned->filterHost($host);
@@ -245,7 +280,8 @@ public function withHost($host): self
/**
* @inheritDoc
*/
- public function withPort($port): self
+ #[Override]
+ public function withPort(?int $port): self
{
$cloned = clone $this;
$cloned->port = $cloned->filterPort($port);
@@ -256,12 +292,9 @@ public function withPort($port): self
/**
* @inheritDoc
*/
- public function withPath($path): self
+ #[Override]
+ public function withPath(array | bool | int | string $path): self
{
- if (!is_string($path)) {
- throw new InvalidArgumentException('path must be a string value');
- }
-
$cloned = clone $this;
$cloned->path = $cloned->filterPath($path);
@@ -271,7 +304,8 @@ public function withPath($path): self
/**
* @inheritDoc
*/
- public function withQuery($query): self
+ #[Override]
+ public function withQuery(string $query): self
{
$cloned = clone $this;
$cloned->query = $cloned->filterQueryOrFragment($query);
@@ -283,7 +317,8 @@ public function withQuery($query): self
* @inheritDoc
* @throws InvalidArgumentException
*/
- public function withFragment($fragment): self
+ #[Override]
+ public function withFragment(string $fragment): self
{
$cloned = clone $this;
$cloned->fragment = $cloned->filterQueryOrFragment($fragment);
@@ -292,18 +327,13 @@ public function withFragment($fragment): self
}
/**
- * Filter and validate the port
+ * Filter and validate the port.
*
- * @param $port
* @throws InvalidArgumentException
*/
- private function filterPort($port): ?int
+ private function filterPort(?int $port): ?int
{
if ($port !== null) {
- if (!is_int($port)) {
- throw new InvalidArgumentException('port must be a integer value');
- }
-
if ($port < 0 || $port > 65535) {
throw new InvalidArgumentException("port: $port must be in a range between 0 and 65535");
}
@@ -313,74 +343,55 @@ private function filterPort($port): ?int
}
/**
- * Filter and validate the scheme
+ * Filter and validate the scheme.
*
- * @param $scheme
* @throws InvalidArgumentException
*/
- private function filterScheme($scheme): string
+ private function filterScheme(string $scheme): string
{
- if (!is_string($scheme)) {
- throw new InvalidArgumentException('scheme must be a lowercase string');
- }
-
return strtolower(trim($scheme));
}
/**
- * Filter and validate the host
+ * Filter and validate the host.
*
- * @param $host
* @throws InvalidArgumentException
*/
- private function filterHost($host): string
+ private function filterHost(string $host): string
{
- if (!is_string($host)) {
- throw new InvalidArgumentException('host must be a string value');
- }
-
return strtolower(trim($host));
}
/**
- * Filter, validate and encode the path
+ * Filter, validate and encode the path.
*
- * @param $path
* @throws InvalidArgumentException
*/
- private function filterPath($path): string
+ private function filterPath(array | bool | int | string $path): string
{
if (!is_string($path)) {
throw new InvalidArgumentException('path must be a string');
}
- $pattern = "/(?:[^" . self::UNRESERVED . self::DELIMITER . "%:@\/]++|%(?![A-Fa-f0-9]{2}))/";
+ $pattern = '/(?:[^' . self::UNRESERVED . self::DELIMITER . "%:@\/]++|%(?![A-Fa-f0-9]{2}))/";
return preg_replace_callback($pattern, [$this, 'encode'], $path);
}
/**
- * * Filter, validate and encode the query or fragment
+ * * Filter, validate and encode the query or fragment.
*
- * @param $fragment
* @throws InvalidArgumentException
*/
- private function filterQueryOrFragment($fragment): string
+ private function filterQueryOrFragment(string $fragment): string
{
- if (!is_string($fragment)) {
- throw new InvalidArgumentException('fragment must be a string');
- }
-
$pattern = '/(?:[^' . self::UNRESERVED . self::DELIMITER . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/';
return preg_replace_callback($pattern, [$this, 'encode'], $fragment);
}
/**
- * Checks if the given scheme uses their standard port
- *
- * @param string $scheme
- * @param int $port
+ * Checks if the given scheme uses their standard port.
*/
private function isStandardPort(string $scheme, ?int $port): bool
{
@@ -392,10 +403,9 @@ private function isStandardPort(string $scheme, ?int $port): bool
}
/**
- * Encoding for path, query and fragment characters
+ * Encoding for path, query and fragment characters.
*
* @param string[] $matches
- * @return string
*/
private function encode(array $matches): string
{
diff --git a/src/Stream/AppendableStream.php b/src/Stream/AppendableStream.php
index 27a827f..b5fdfab 100644
--- a/src/Stream/AppendableStream.php
+++ b/src/Stream/AppendableStream.php
@@ -8,22 +8,21 @@
use Psr\Http\Message\StreamInterface;
/**
- * Interface for appendable streams
+ * Interface for appendable streams.
*/
interface AppendableStream extends StreamInterface
{
/**
- * Append the given stream to this stream and return thr number of byte appended
+ * Append the given stream to this stream and return thr number of byte appended.
*
- * @param AppendableStream $stream
* @throws RuntimeException
*/
public function appendStream(AppendableStream $stream): int;
/**
- * Return the resource handle
+ * Return the resource handle.
*
- * @return resource Stream resource handle
+ * @return resource|null
*/
- public function getResource();
+ public function getResource(): mixed;
}
diff --git a/src/Stream/Stream.php b/src/Stream/Stream.php
index f339a4c..5f31479 100644
--- a/src/Stream/Stream.php
+++ b/src/Stream/Stream.php
@@ -14,15 +14,19 @@
namespace Artemeon\HttpClient\Stream;
use Artemeon\HttpClient\Exception\RuntimeException;
+use Override;
/**
- * Stream interface implementation for large strings and files
+ * Stream interface implementation for large strings and files.
*
* @see https://www.php.net/manual/de/intro.stream.php
*/
class Stream implements AppendableStream
{
- private $resource;
+ /**
+ * @var resource|null
+ */
+ private mixed $resource = null;
/**
* @see https://www.php.net/manual/de/function.stream-get-meta-data
@@ -31,9 +35,10 @@ class Stream implements AppendableStream
/**
* @param resource $resource
+ *
* @throws RuntimeException
*/
- private function __construct($resource)
+ private function __construct(mixed $resource)
{
if (!is_resource($resource)) {
throw new RuntimeException('Invalid resource');
@@ -44,7 +49,7 @@ private function __construct($resource)
}
/**
- * Force to close the file handle
+ * Force to close the file handle.
*/
public function __destruct()
{
@@ -52,15 +57,16 @@ public function __destruct()
}
/**
- * Named constructor to create an instance based on the given string
+ * Named constructor to create an instance based on the given string.
*
* @param string $string String content
* @param string $mode @see https://www.php.net/manual/de/function.fopen.php
+ *
* @throws RuntimeException
*/
public static function fromString(string $string, string $mode = 'r+'): self
{
- $resource = fopen("php://temp", $mode);
+ $resource = fopen('php://temp', $mode);
$instance = new self($resource);
$instance->write($string);
@@ -68,22 +74,25 @@ public static function fromString(string $string, string $mode = 'r+'): self
}
/**
- * Named constructor to create an instance based on the given file mode
+ * Named constructor to create an instance based on the given file mode.
*
* @param string $mode Stream Modes: @see https://www.php.net/manual/de/function.fopen.php
+ *
* @throws RuntimeException
*/
public static function fromFileMode(string $mode): self
{
- $resource = fopen("php://temp", $mode);
+ $resource = fopen('php://temp', $mode);
+
return new self($resource);
}
/**
- * Named constructor to create an instance based on the given file and read/write mode
+ * Named constructor to create an instance based on the given file and read/write mode.
*
* @param string $file Path to the file
* @param string $mode Stream Modes: @see https://www.php.net/manual/de/function.fopen.php
+ *
* @throws RuntimeException
*/
public static function fromFile(string $file, string $mode = 'r+'): self
@@ -91,7 +100,7 @@ public static function fromFile(string $file, string $mode = 'r+'): self
$resource = fopen($file, $mode);
if (!is_resource($resource)) {
- throw new RuntimeException("Cam't open file $file");
+ throw new RuntimeException("Can't open file $file");
}
return new self($resource);
@@ -100,12 +109,13 @@ public static function fromFile(string $file, string $mode = 'r+'): self
/**
* @inheritDoc
*/
- public function __toString()
+ #[Override]
+ public function __toString(): string
{
try {
$this->rewind();
$content = $this->getContents();
- } catch (RuntimeException $exception) {
+ } catch (RuntimeException) {
$content = '';
}
@@ -115,6 +125,7 @@ public function __toString()
/**
* @inheritDoc
*/
+ #[Override]
public function appendStream(AppendableStream $stream): int
{
$this->assertStreamIsNotDetached();
@@ -125,10 +136,10 @@ public function appendStream(AppendableStream $stream): int
}
$stream->rewind();
- $bytes = stream_copy_to_stream($stream->getResource(), $this->resource);
+ $bytes = stream_copy_to_stream($stream->getResource(), $this->getResource());
if ($bytes === false) {
- throw new RuntimeException("Append failed");
+ throw new RuntimeException('Append failed');
}
return $bytes;
@@ -137,7 +148,8 @@ public function appendStream(AppendableStream $stream): int
/**
* @inheritDoc
*/
- public function getResource()
+ #[Override]
+ public function getResource(): mixed
{
return $this->resource;
}
@@ -145,7 +157,8 @@ public function getResource()
/**
* @inheritDoc
*/
- public function close()
+ #[Override]
+ public function close(): void
{
if (!is_resource($this->resource)) {
return;
@@ -157,17 +170,21 @@ public function close()
/**
* @inheritDoc
*/
+ #[Override]
public function detach()
{
$this->close();
$this->metaData = [];
$this->resource = null;
+
+ return null;
}
/**
* @inheritDoc
*/
- public function getSize()
+ #[Override]
+ public function getSize(): ?int
{
if (!is_resource($this->resource)) {
return null;
@@ -175,28 +192,30 @@ public function getSize()
$fstat = fstat($this->resource);
- return ($fstat['size']);
+ return $fstat['size'];
}
/**
* @inheritDoc
*/
- public function tell()
+ #[Override]
+ public function tell(): int
{
$this->assertStreamIsNotDetached();
- $position = ftell($this->resource);
+ $position = ftell($this->getResource());
if ($position === false) {
throw new RuntimeException("Can't determine position");
}
- return (int)$position;
+ return (int) $position;
}
/**
* @inheritDoc
*/
- public function eof()
+ #[Override]
+ public function eof(): bool
{
if (!is_resource($this->resource)) {
// php.net doc: feof returns TRUE if the file pointer is at EOF or an error occurs
@@ -209,7 +228,8 @@ public function eof()
/**
* @inheritDoc
*/
- public function isSeekable()
+ #[Override]
+ public function isSeekable(): bool
{
if (!is_resource($this->resource)) {
return false;
@@ -217,31 +237,33 @@ public function isSeekable()
// According to the fopen manual mode 'a' and 'a+' are not seekable
foreach (['a', 'a+'] as $mode) {
- if (strpos($this->metaData["mode"], $mode) !== false) {
+ if (str_contains((string) $this->metaData['mode'], $mode)) {
return false;
}
}
- return (bool)$this->getMetadata('seekable');
+ return (bool) $this->getMetadata('seekable');
}
/**
* @inheritDoc
*/
- public function seek($offset, $whence = SEEK_SET)
+ #[Override]
+ public function seek(int $offset, int $whence = SEEK_SET): void
{
$this->assertStreamIsNotDetached();
$result = fseek($this->resource, $offset, $whence);
if ($result === -1) {
- throw new RuntimeException("Cant't seek with offset $offset");
+ throw new RuntimeException("Can't seek with offset $offset");
}
}
/**
* @inheritDoc
*/
- public function rewind()
+ #[Override]
+ public function rewind(): void
{
$this->assertStreamIsNotDetached();
@@ -255,15 +277,16 @@ public function rewind()
/**
* @inheritDoc
*/
- public function write($string)
+ #[Override]
+ public function write(string $string): int
{
$this->assertStreamIsNotDetached();
$this->assertStreamIsWriteable();
- $bytes = fwrite($this->resource, strval($string));
+ $bytes = fwrite($this->getResource(), $string);
if ($bytes === false) {
- throw new RuntimeException("Cant't write to stream");
+ throw new RuntimeException("Can't write to stream");
}
return $bytes;
@@ -272,7 +295,8 @@ public function write($string)
/**
* @inheritDoc
*/
- public function isWritable()
+ #[Override]
+ public function isWritable(): bool
{
if (!is_resource($this->resource)) {
return false;
@@ -281,7 +305,7 @@ public function isWritable()
$writeModes = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'];
foreach ($writeModes as $mode) {
- if (strpos($this->metaData["mode"], $mode) !== false) {
+ if (str_contains((string) $this->metaData['mode'], $mode)) {
return true;
}
}
@@ -292,7 +316,8 @@ public function isWritable()
/**
* @inheritDoc
*/
- public function isReadable()
+ #[Override]
+ public function isReadable(): bool
{
if (!is_resource($this->resource)) {
return false;
@@ -301,7 +326,7 @@ public function isReadable()
$readModes = ['r', 'r+', 'w+', 'a+', 'x+', 'c+'];
foreach ($readModes as $mode) {
- if (strpos($this->metaData["mode"], $mode) !== false) {
+ if (str_contains((string) $this->metaData['mode'], $mode)) {
return true;
}
}
@@ -312,15 +337,16 @@ public function isReadable()
/**
* @inheritDoc
*/
- public function read($length)
+ #[Override]
+ public function read(int $length): string
{
$this->assertStreamIsNotDetached();
$this->assertStreamIsReadable();
- $string = fread($this->resource, intval($length));
+ $string = fread($this->getResource(), $length);
if ($string === false) {
- throw new RuntimeException("Can't read from stream");
+ throw new RuntimeException("Can't read from stream");
}
return $string;
@@ -332,15 +358,16 @@ public function read($length)
* This function reads the complete stream from the CURRENT! file pointer. If you
* want ensure to read the complete stream use __toString() instead.
*/
- public function getContents()
+ #[Override]
+ public function getContents(): string
{
$this->assertStreamIsNotDetached();
$this->assertStreamIsReadable();
- $content = stream_get_contents($this->resource);
+ $content = stream_get_contents($this->getResource());
if ($content === false) {
- throw new RuntimeException("Can't read content from stream");
+ throw new RuntimeException("Can't read content from stream");
}
return $content;
@@ -349,17 +376,14 @@ public function getContents()
/**
* @inheritDoc
*/
- public function getMetadata($key = null)
+ #[Override]
+ public function getMetadata(?string $key = null): mixed
{
if ($key === null) {
return $this->metaData;
}
- if (isset($this->metaData[$key])) {
- return $this->metaData[$key];
- }
-
- return null;
+ return $this->metaData[$key] ?? null;
}
/**
@@ -367,7 +391,7 @@ public function getMetadata($key = null)
*/
private function assertStreamIsNotDetached(): void
{
- if ($this->resource === null) {
+ if ($this->getResource() === null) {
throw new RuntimeException('Stream is detached');
}
}
diff --git a/tests/Integration/RequestTest.php b/tests/Integration/RequestTest.php
index 6203cfb..de408ca 100644
--- a/tests/Integration/RequestTest.php
+++ b/tests/Integration/RequestTest.php
@@ -6,28 +6,191 @@
use Artemeon\HttpClient\Http\Request;
use Artemeon\HttpClient\Http\Uri;
+use Artemeon\HttpClient\Tests\TestCase;
+use GuzzleHttp\Psr7\MessageTrait;
+use GuzzleHttp\Psr7\Uri as GuzzleUri;
use GuzzleHttp\Psr7\Utils;
-use Http\Psr7Test\RequestIntegrationTest;
+use PHPUnit\Framework\Attributes\CoversClass;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\StreamInterface;
+use RuntimeException;
/**
- * @covers \Artemeon\HttpClient\Http\Request
+ * @internal
*/
-class RequestTest extends RequestIntegrationTest
+#[CoversClass(Request::class)]
+class RequestTest extends TestCase
{
+ use MessageTrait;
+
+ protected function buildUri($uri): GuzzleUri
+ {
+ if (class_exists(GuzzleUri::class)) {
+ return new GuzzleUri($uri);
+ }
+
+ throw new RuntimeException('Could not create URI. Check your config');
+ }
+
/**
- * Overwrite, parent code doesn't work witz Guzzle > 7.2, remove when paren code is fixed
+ * Overwrite, parent code doesn't work witz Guzzle > 7.2, remove when paren code is fixed.
*/
- protected function buildStream($data)
+ protected function buildStream($data): StreamInterface
{
return Utils::streamFor($data);
}
+ public function createSubject(): Request
+ {
+ $this->skippedTests['testMethodIsExtendable'] = '';
+
+ return Request::forGet(Uri::fromString('/'));
+ }
+
+ /**
+ * @var array with functionName => reason
+ */
+ protected $skippedTests = [];
+
/**
- * @inheritDoc
+ * @var RequestInterface
*/
- public function createSubject()
+ protected $request;
+
+ protected function setUp(): void
{
- $this->skippedTests['testMethodIsExtendable'] = "";
- return Request::forGet(Uri::fromString('/'));
+ $this->request = $this->createSubject();
+ }
+
+ protected function getMessage()
+ {
+ return $this->request;
+ }
+
+ public function testRequestTarget(): void
+ {
+ if (isset($this->skippedTests[__FUNCTION__])) {
+ $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+ }
+
+ $original = clone $this->request;
+ $this->assertEquals('/', $this->request->getRequestTarget());
+
+ $request = $this->request->withRequestTarget('*');
+ $this->assertNotSame($this->request, $request);
+ $this->assertEquals($this->request, $original, 'Request object MUST not be mutated');
+ $this->assertEquals('*', $request->getRequestTarget());
+ }
+
+ public function testMethod(): void
+ {
+ if (isset($this->skippedTests[__FUNCTION__])) {
+ $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+ }
+
+ $this->assertEquals('GET', $this->request->getMethod());
+ $original = clone $this->request;
+
+ $request = $this->request->withMethod('POST');
+ $this->assertNotSame($this->request, $request);
+ $this->assertEquals($this->request, $original, 'Request object MUST not be mutated');
+ $this->assertEquals('POST', $request->getMethod());
+ }
+
+ public function testMethodIsCaseSensitive(): void
+ {
+ if (isset($this->skippedTests[__FUNCTION__])) {
+ $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+ }
+
+ $request = $this->request->withMethod('head');
+ $this->assertEquals('head', $request->getMethod());
+ }
+
+ public function testMethodIsExtendable(): void
+ {
+ if (isset($this->skippedTests[__FUNCTION__])) {
+ $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+ }
+
+ $request = $this->request->withMethod('CUSTOM');
+ $this->assertEquals('CUSTOM', $request->getMethod());
+ }
+
+ public function testUri(): void
+ {
+ if (isset($this->skippedTests[__FUNCTION__])) {
+ $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+ }
+ $original = clone $this->request;
+
+ $uri = $this->buildUri('http://www.foo.com/bar');
+ $request = $this->request->withUri($uri);
+ $this->assertNotSame($this->request, $request);
+ $this->assertEquals($this->request, $original, 'Request object MUST not be mutated');
+ $this->assertEquals('www.foo.com', $request->getHeaderLine('host'));
+ $this->assertEquals('http://www.foo.com/bar', (string) $request->getUri());
+
+ $request = $request->withUri($this->buildUri('/foobar'));
+ $this->assertNotSame($this->request, $request);
+ $this->assertEquals($this->request, $original, 'Request object MUST not be mutated');
+ $this->assertEquals('www.foo.com', $request->getHeaderLine('host'), 'If the URI does not contain a host component, any pre-existing Host header MUST be carried over to the returned request.');
+ $this->assertEquals('/foobar', (string) $request->getUri());
+ }
+
+ public function testUriPreserveHostNoHostHost(): void
+ {
+ if (isset($this->skippedTests[__FUNCTION__])) {
+ $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+ }
+
+ $request = $this->request->withUri($this->buildUri('http://www.foo.com/bar'), true);
+ $this->assertEquals('www.foo.com', $request->getHeaderLine('host'));
+ }
+
+ public function testUriPreserveHostNoHostNoHost(): void
+ {
+ if (isset($this->skippedTests[__FUNCTION__])) {
+ $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+ }
+
+ $host = $this->request->getHeaderLine('host');
+ $request = $this->request->withUri($this->buildUri('/bar'), true);
+ $this->assertEquals($host, $request->getHeaderLine('host'));
+ }
+
+ public function testUriPreserveHostHostHost(): void
+ {
+ if (isset($this->skippedTests[__FUNCTION__])) {
+ $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+ }
+
+ $request = $this->request->withUri($this->buildUri('http://www.foo.com/bar'));
+ $host = $request->getHeaderLine('host');
+
+ $request2 = $request->withUri($this->buildUri('http://www.bar.com/foo'), true);
+ $this->assertEquals($host, $request2->getHeaderLine('host'));
+ }
+
+ /**
+ * psr7-integration-tests
+ * Tests that getRequestTarget(), when using the default behavior of
+ * displaying the origin-form, normalizes multiple leading slashes in the
+ * path to a single slash. This is done to prevent URL poisoning and/or XSS
+ * issues.
+ *
+ * @see UriIntegrationTest::testGetPathNormalizesMultipleLeadingSlashesToSingleSlashToPreventXSS
+ */
+ public function testGetRequestTargetInOriginFormNormalizesUriWithMultipleLeadingSlashesInPath(): void
+ {
+ if (isset($this->skippedTests[__FUNCTION__])) {
+ $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+ }
+
+ $url = 'http://example.org//valid///path';
+ $request = $this->request->withUri($this->buildUri($url));
+ $requestTarget = $request->getRequestTarget();
+
+ $this->assertSame('/valid///path', $requestTarget);
}
}
diff --git a/tests/Integration/ResponseTest.php b/tests/Integration/ResponseTest.php
index 9adc580..2b425c7 100644
--- a/tests/Integration/ResponseTest.php
+++ b/tests/Integration/ResponseTest.php
@@ -5,27 +5,61 @@
namespace Artemeon\HttpClient\Tests\Integration;
use Artemeon\HttpClient\Http\Response;
+use Artemeon\HttpClient\Tests\TestCase;
use GuzzleHttp\Psr7\Utils;
-use Http\Psr7Test\ResponseIntegrationTest;
/**
- * @covers \Artemeon\HttpClient\Http\Response
+ * @internal
*/
-class ResponseTest extends ResponseIntegrationTest
+#[\PHPUnit\Framework\Attributes\CoversClass(Response::class)]
+class ResponseTest extends TestCase
{
+ private Response $response;
+
/**
- * Overwrite, parent code doesn't work witz Guzzle > 7.2, remove when paren code is fixed
+ * Overwrite, parent code doesn't work witz Guzzle > 7.2, remove when paren code is fixed.
*/
protected function buildStream($data)
{
return Utils::streamFor($data);
}
- /**
- * @inheritDoc
- */
- public function createSubject()
+ public function createSubject(): Response
{
return new Response(200, '1.1');
}
+
+ protected function setUp(): void
+ {
+ $this->response = $this->createSubject();
+ }
+
+ protected function getMessage()
+ {
+ return $this->response;
+ }
+
+ public function testStatusCode(): void
+ {
+ if (isset($this->skippedTests[__FUNCTION__])) {
+ $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+ }
+
+ $original = clone $this->response;
+ $response = $this->response->withStatus(204);
+ $this->assertNotSame($this->response, $response);
+ $this->assertEquals($this->response, $original, 'Response MUST not be mutated');
+ $this->assertSame(204, $response->getStatusCode());
+ }
+
+ public function testReasonPhrase(): void
+ {
+ if (isset($this->skippedTests[__FUNCTION__])) {
+ $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+ }
+
+ $response = $this->response->withStatus(204, 'Foobar');
+ $this->assertSame(204, $response->getStatusCode());
+ $this->assertEquals('Foobar', $response->getReasonPhrase());
+ }
}
diff --git a/tests/Integration/UriTest.php b/tests/Integration/UriTest.php
index f934df5..0a6f7c7 100644
--- a/tests/Integration/UriTest.php
+++ b/tests/Integration/UriTest.php
@@ -5,18 +5,81 @@
namespace Artemeon\HttpClient\Tests\Integration;
use Artemeon\HttpClient\Http\Uri;
-use Http\Psr7Test\UriIntegrationTest;
+use Artemeon\HttpClient\Tests\TestCase;
+use Psr\Http\Message\UriInterface;
/**
- * @covers \Artemeon\HttpClient\Http\Uri
+ * @internal
*/
-class UriTest extends UriIntegrationTest
+#[\PHPUnit\Framework\Attributes\CoversClass(Uri::class)]
+class UriTest extends TestCase
{
+ public function createUri(string $uri)
+ {
+ return Uri::fromString($uri);
+ }
+
/**
- * @inheritDoc
+ * Tests that getPath() normalizes multiple leading slashes to a single
+ * slash. This is done to ensure that when a path is used in isolation from
+ * the authority, it will not cause URL poisoning and/or XSS issues.
+ *
+ * @see https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-3257
+ *
+ * @psalm-param array{expected: non-empty-string, uri: UriInterface} $test
*/
- public function createUri($uri)
+ public function testGetPathNormalizesMultipleLeadingSlashesToSingleSlashToPreventXSS()
{
- return Uri::fromString($uri);
+ if (isset($this->skippedTests[__FUNCTION__])) {
+ $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+ }
+
+ $expected = 'http://example.org//valid///path';
+ $uri = $this->createUri($expected);
+
+ $this->assertInstanceOf(UriInterface::class, $uri);
+ $this->assertSame('/valid///path', $uri->getPath());
+
+ return [
+ 'expected' => $expected,
+ 'uri' => $uri,
+ ];
+ }
+
+ /**
+ * Tests that the full string representation of a URI that includes multiple
+ * leading slashes in the path is presented verbatim (in contrast to what is
+ * provided when calling getPath()).
+ *
+ * @psalm-param array{expected: non-empty-string, uri: UriInterface} $test
+ */
+ #[\PHPUnit\Framework\Attributes\Depends('testGetPathNormalizesMultipleLeadingSlashesToSingleSlashToPreventXSS')]
+ public function testStringRepresentationWithMultipleSlashes(array $test): void
+ {
+ $this->assertSame($test['expected'], (string) $test['uri']);
+ }
+
+ /**
+ * Tests that special chars in `userInfo` must always be URL-encoded to pass RFC3986 compliant URIs where characters
+ * in username and password MUST NOT contain reserved characters.
+ *
+ * This test is taken from {@see https://github.com/guzzle/psr7/blob/3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf/tests/UriTest.php#L679-L688 guzzlehttp/psr7}.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc3986#appendix-A
+ */
+ public function testSpecialCharsInUserInfo(): void
+ {
+ $uri = $this->createUri('/')->withUserInfo('foo@bar.com', 'pass#word');
+ self::assertSame('foo%40bar.com:pass%23word', $uri->getUserInfo());
+ }
+
+ /**
+ * Tests that userinfo which is already encoded is not encoded twice.
+ * This test is taken from {@see https://github.com/guzzle/psr7/blob/3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf/tests/UriTest.php#L679-L688 guzzlehttp/psr7}.
+ */
+ public function testAlreadyEncodedUserInfo(): void
+ {
+ $uri = $this->createUri('/')->withUserInfo('foo%40bar.com', 'pass%23word');
+ self::assertSame('foo%40bar.com:pass%23word', $uri->getUserInfo());
}
}
diff --git a/tests/Pest.php b/tests/Pest.php
new file mode 100644
index 0000000..8c2ce0a
--- /dev/null
+++ b/tests/Pest.php
@@ -0,0 +1,36 @@
+in('Feature');
+
+/*
+|--------------------------------------------------------------------------
+| Expectations
+|--------------------------------------------------------------------------
+|
+| When you're writing tests, you often need to check that values meet certain conditions. The
+| "expect()" function gives you access to a set of "expectations" methods that you can use
+| to assert different things. Of course, you may extend the Expectation API at any time.
+|
+*/
+
+/*
+|--------------------------------------------------------------------------
+| Functions
+|--------------------------------------------------------------------------
+|
+| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
+| project that you don't want to repeat in every file. Here you can also expose helpers as
+| global functions to help you to reduce the number of lines of code in your test files.
+|
+*/
diff --git a/tests/System/endpoints/upload.php b/tests/System/endpoints/upload.php
index 12aad13..1917f1b 100644
--- a/tests/System/endpoints/upload.php
+++ b/tests/System/endpoints/upload.php
@@ -2,8 +2,8 @@
declare(strict_types=1);
-header("HTTP/1.0 200 OK");
-header("Content-Type: text/plain");
+header('HTTP/1.0 200 OK');
+header('Content-Type: text/plain');
echo '
TARGET SERVER:
';
echo 'REQUEST
';
print_r($_REQUEST);
diff --git a/tests/System/test_get.php b/tests/System/test_get.php
index 6508a8e..f58b76c 100644
--- a/tests/System/test_get.php
+++ b/tests/System/test_get.php
@@ -28,4 +28,3 @@
} catch (HttpClientException $exception) {
print_r($exception);
}
-
diff --git a/tests/System/test_post-url-encoded_form.php b/tests/System/test_post-url-encoded_form.php
index 4402d69..d9ff57a 100644
--- a/tests/System/test_post-url-encoded_form.php
+++ b/tests/System/test_post-url-encoded_form.php
@@ -27,8 +27,8 @@
try {
$request = Request::forPost(
Uri::fromString('http://apache/endpoints/upload.php'),
- Body::fromEncoder(FormUrlEncoder::fromArray(["username" => 'john.doe'])),
- Headers::fromFields([Authorization::forAuthBasic('john.doe', 'geheim')])
+ Body::fromEncoder(FormUrlEncoder::fromArray(['username' => 'john.doe'])),
+ Headers::fromFields([Authorization::forAuthBasic('john.doe', 'geheim')]),
);
HttpClientTestFactory::withTransactionLog()->send($request);
@@ -36,4 +36,3 @@
} catch (HttpClientException $exception) {
print_r($exception);
}
-
diff --git a/tests/System/test_post_json_with_auth.php b/tests/System/test_post_json_with_auth.php
index 3dfe925..0996f25 100644
--- a/tests/System/test_post_json_with_auth.php
+++ b/tests/System/test_post_json_with_auth.php
@@ -28,7 +28,7 @@
$request = Request::forPost(
Uri::fromString('http://apache/endpoints/upload.php'),
Body::fromReader(FileReader::fromFile('../Fixtures/encoder/generated.json')),
- Headers::fromFields([Authorization::forAuthBasic('John.Doe', 'geheim')])
+ Headers::fromFields([Authorization::forAuthBasic('John.Doe', 'geheim')]),
);
HttpClientTestFactory::withTransactionLog()->send($request);
diff --git a/tests/System/test_post_multipart.php b/tests/System/test_post_multipart.php
index 3efcb97..f9dfa86 100644
--- a/tests/System/test_post_multipart.php
+++ b/tests/System/test_post_multipart.php
@@ -30,8 +30,8 @@
MultipartFormDataEncoder::create()
->addFieldPart('user', 'John.Doe')
->addFieldPart('password', mb_convert_encoding('geheim', 'UTF-8', 'ISO-8859-1'))
- ->addFilePart('user_image', 'header_logo.png', Stream::fromFile('../Fixtures/reader/header_logo.png'))
- )
+ ->addFilePart('user_image', 'header_logo.png', Stream::fromFile('../Fixtures/reader/header_logo.png')),
+ ),
);
HttpClientTestFactory::withTransactionLog()->send($request);
@@ -39,4 +39,3 @@
} catch (HttpClientException $exception) {
print_r($exception);
}
-
diff --git a/tests/System/test_put_with_config.php b/tests/System/test_put_with_config.php
index 1ead75c..a1cb6bb 100644
--- a/tests/System/test_put_with_config.php
+++ b/tests/System/test_put_with_config.php
@@ -32,18 +32,18 @@
Body::fromEncoder(
FormUrlEncoder::fromArray(
[
- "user" => 'John.Doe',
+ 'user' => 'John.Doe',
'password' => 'geheim',
'group' => 'admin',
- ]
- )
+ ],
+ ),
),
Headers::fromFields(
[
Authorization::forAuthBasic('John.Doe', 'geheim'),
UserAgent::fromString(),
- ]
- )
+ ],
+ ),
);
$clientOptions = ClientOptions::fromDefaults();
diff --git a/tests/System/test_token.php b/tests/System/test_token.php
index e646971..aad065b 100644
--- a/tests/System/test_token.php
+++ b/tests/System/test_token.php
@@ -33,12 +33,12 @@
200,
'1.1',
Stream::fromString(
- '{"access_token": "PQtdWwDDESjpSyYnDAerj92O3sHWlZ", "expires_in": 7884000, "token_type": "Bearer", "scope": "read_suppliers"}'
+ '{"access_token": "PQtdWwDDESjpSyYnDAerj92O3sHWlZ", "expires_in": 7884000, "token_type": "Bearer", "scope": "read_suppliers"}',
),
- Headers::fromFields([ContentType::fromString(MediaType::JSON)])
+ Headers::fromFields([ContentType::fromString(MediaType::JSON)]),
),
new Response(200, '1.1', Stream::fromString('It works')),
- ]
+ ],
);
try {
@@ -46,18 +46,16 @@
ClientCredentials::forHeaderAuthorization(
'yoour_client_id',
'your_client_secret',
- 'read_suppliers'
+ 'read_suppliers',
),
Uri::fromString('https://api.lbbw-test.prospeum.com/o/token/'),
- HttpClientTestFactory::withMockHandler()
+ HttpClientTestFactory::withMockHandler(),
);
$response = $apiClient->send(
- Request::forGet(Uri::fromString('https://api.lbbw-test.prospeum.com/api/v01/supplier/search/'))
+ Request::forGet(Uri::fromString('https://api.lbbw-test.prospeum.com/api/v01/supplier/search/')),
);
echo $response->getBody()->__toString();
} catch (HttpClientException $exception) {
print_r($exception);
}
-
-
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..d60b23e
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,9 @@
+mockHandler = new MockHandler();
$this->guzzleClient = new GuzzleClient(['handler' => HandlerStack::create($this->mockHandler)]);
- $this->clientOptionsConverter = $this->prophesize(ClientOptionsConverter::class);
+ $this->clientOptionsConverter = Mockery::mock(ClientOptionsConverter::class);
$this->clientOptions = ClientOptions::fromDefaults();
$this->httpClient = new ArtemeonHttpClient(
$this->guzzleClient,
- $this->clientOptionsConverter->reveal()
+ $this->clientOptionsConverter,
);
}
- /**
- * @test
- */
- public function send_WithoutOptions_UsesEmptyOptionsArray()
+ public function testSendWithoutOptionsUsesEmptyOptionsArray(): void
{
$this->mockHandler->append(new GuzzleResponse(200, [], 'Some body content'));
- $this->clientOptionsConverter->toGuzzleOptionsArray(Argument::any())->shouldNotBeCalled();
+ $this->clientOptionsConverter->shouldNotReceive('toGuzzleOptionsArray');
$request = Request::forGet(Uri::fromString('http://apache/'));
$response = $this->httpClient->send($request);
@@ -93,15 +87,13 @@ public function send_WithoutOptions_UsesEmptyOptionsArray()
self::assertInstanceOf(Response::class, $response);
}
- /**
- * @test
- */
- public function send_WithOptions_ConvertOptions()
+ public function testSendWithOptionsConvertOptions(): void
{
$this->mockHandler->append(new GuzzleResponse(200, [], 'Some body content'));
- $this->clientOptionsConverter->toGuzzleOptionsArray($this->clientOptions)
- ->shouldBeCalled()
- ->willReturn([]);
+ $this->clientOptionsConverter->shouldReceive('toGuzzleOptionsArray')
+ ->withArgs([$this->clientOptions])
+ ->once()
+ ->andReturn([]);
$request = Request::forGet(Uri::fromString('http://apache/'));
$response = $this->httpClient->send($request, $this->clientOptions);
@@ -109,10 +101,7 @@ public function send_WithOptions_ConvertOptions()
self::assertInstanceOf(Response::class, $response);
}
- /**
- * @test
- */
- public function send_ConvertsGuzzleResponseToValidResponse(): void
+ public function testSendConvertsGuzzleResponseToValidResponse(): void
{
$request = Request::forGet(Uri::fromString('http://apache/endpoints/upload.php'));
$expectedContent = 'Some body content';
@@ -126,14 +115,11 @@ public function send_ConvertsGuzzleResponseToValidResponse(): void
self::assertSame($expectedHeaders, $response->getHeaders());
}
- /**
- * @test
- * @dataProvider provideExceptionMappings
- */
- public function send_GuzzleThrowsException_MappedToHttpClientException(
+ #[DataProvider('provideExceptionMappings')]
+ public function testSendGuzzleThrowsExceptionMappedToHttpClientException(
\RuntimeException $guzzleException,
- string $httpClientException
- ) {
+ string $httpClientException,
+ ): void {
$this->mockHandler->append($guzzleException);
$request = Request::forGet(Uri::fromString('http://apache/endpoints/upload.php'));
@@ -142,9 +128,9 @@ public function send_GuzzleThrowsException_MappedToHttpClientException(
}
/**
- * Data provider for exception mappings from guzzle to httpClient exceptions
+ * Data provider for exception mappings from guzzle to httpClient exceptions.
*/
- public function provideExceptionMappings(): array
+ public static function provideExceptionMappings(): array
{
$fakeResponse = new GuzzleResponse(200);
$fakeRequest = new GuzzleRequest('GET', 'test');
diff --git a/tests/Unit/Client/ClientOptionsConverterTest.php b/tests/Unit/Client/ClientOptionsConverterTest.php
index 91a0515..45de6cb 100644
--- a/tests/Unit/Client/ClientOptionsConverterTest.php
+++ b/tests/Unit/Client/ClientOptionsConverterTest.php
@@ -16,32 +16,28 @@
use Artemeon\HttpClient\Client\Options\ClientOptions;
use Artemeon\HttpClient\Client\Options\ClientOptionsConverter;
use GuzzleHttp\RequestOptions as GuzzleRequestOptions;
+use Override;
use PHPUnit\Framework\TestCase;
-use Prophecy\PhpUnit\ProphecyTrait;
/**
- * @covers \Artemeon\HttpClient\Client\Options\ClientOptionsConverter
+ * @internal
*/
class ClientOptionsConverterTest extends TestCase
{
- use ProphecyTrait;
-
private ClientOptionsConverter $clientOptionConverter;
private ClientOptions $clientOptions;
/**
* @inheritDoc
*/
+ #[Override]
public function setUp(): void
{
$this->clientOptions = ClientOptions::fromDefaults();
$this->clientOptionConverter = new ClientOptionsConverter();
}
- /**
- * @test
- */
- public function verifyKey_IsFalse(): void
+ public function testVerifyKeyIsFalse(): void
{
$this->clientOptions->optDisableSslVerification();
$options = $this->clientOptionConverter->toGuzzleOptionsArray($this->clientOptions);
@@ -49,20 +45,14 @@ public function verifyKey_IsFalse(): void
self::assertFalse($options[GuzzleRequestOptions::VERIFY]);
}
- /**
- * @test
- */
- public function verifyKey_IsTrue(): void
+ public function testVerifyKeyIsTrue(): void
{
$options = $this->clientOptionConverter->toGuzzleOptionsArray($this->clientOptions);
self::assertTrue($options[GuzzleRequestOptions::VERIFY]);
}
- /**
- * @test
- */
- public function verifyKey_IsCaBundlePathString(): void
+ public function testVerifyKeyIsCaBundlePathString(): void
{
$expected = '/path/ca/bundle';
$this->clientOptions->optSetCustomCaBundlePath($expected);
@@ -71,10 +61,7 @@ public function verifyKey_IsCaBundlePathString(): void
self::assertSame($expected, $options[GuzzleRequestOptions::VERIFY]);
}
- /**
- * @test
- */
- public function allowRedirectsKey_ReturnFalse(): void
+ public function testAllowRedirectsKeyReturnFalse(): void
{
$this->clientOptions->optDisableRedirects();
$options = $this->clientOptionConverter->toGuzzleOptionsArray($this->clientOptions);
@@ -82,10 +69,7 @@ public function allowRedirectsKey_ReturnFalse(): void
self::assertFalse($options[GuzzleRequestOptions::ALLOW_REDIRECTS]);
}
- /**
- * @test
- */
- public function allowRedirectsKey_ReturnsValidArray(): void
+ public function testAllowRedirectsKeyReturnsValidArray(): void
{
$expectedMax = 10;
$expectedReferer = true;
@@ -98,10 +82,7 @@ public function allowRedirectsKey_ReturnsValidArray(): void
self::assertSame($expectedMax, $options[GuzzleRequestOptions::ALLOW_REDIRECTS]['max']);
}
- /**
- * @test
- */
- public function timeoutKey_HasExpectedIntValue(): void
+ public function testTimeoutKeyHasExpectedIntValue(): void
{
$expected = 22;
$this->clientOptions->optSetTimeout($expected);
diff --git a/tests/Unit/Client/ClientOptionsTest.php b/tests/Unit/Client/ClientOptionsTest.php
index bbe2257..ee3a996 100644
--- a/tests/Unit/Client/ClientOptionsTest.php
+++ b/tests/Unit/Client/ClientOptionsTest.php
@@ -14,30 +14,26 @@
namespace Artemeon\HttpClient\Tests\Unit\Client;
use Artemeon\HttpClient\Client\Options\ClientOptions;
+use Override;
use PHPUnit\Framework\TestCase;
-use Prophecy\PhpUnit\ProphecyTrait;
/**
- * @covers \Artemeon\HttpClient\Client\Options\ClientOptions
+ * @internal
*/
class ClientOptionsTest extends TestCase
{
- use ProphecyTrait;
-
private ClientOptions $clientOptions;
/**
* @inheritDoc
*/
+ #[Override]
public function setUp(): void
{
$this->clientOptions = ClientOptions::fromDefaults();
}
- /**
- * @test
- */
- public function fromDefaults_setValidValues(): void
+ public function testFromDefaultsSetValidValues(): void
{
self::assertTrue($this->clientOptions->isRedirectAllowed());
self::assertSame(10, $this->clientOptions->getTimeout());
@@ -48,10 +44,7 @@ public function fromDefaults_setValidValues(): void
self::assertFalse($this->clientOptions->hasCustomCaBundlePath());
}
- /**
- * @test
- */
- public function ChangedOptions_SetValidValues(): void
+ public function testChangedOptionsSetValidValues(): void
{
$this->clientOptions->optDisableRedirects();
$this->clientOptions->optSetTimeout(50);
diff --git a/tests/Unit/Client/HttpClientLogDecoratorTest.php b/tests/Unit/Client/HttpClientLogDecoratorTest.php
index 59e6fc7..4825edb 100644
--- a/tests/Unit/Client/HttpClientLogDecoratorTest.php
+++ b/tests/Unit/Client/HttpClientLogDecoratorTest.php
@@ -23,97 +23,86 @@
use Artemeon\HttpClient\Http\Request;
use Artemeon\HttpClient\Http\Response;
use Artemeon\HttpClient\Http\Uri;
+use Mockery;
+use Override;
use PHPUnit\Framework\TestCase;
-use Prophecy\Argument;
-use Prophecy\PhpUnit\ProphecyTrait;
-use Prophecy\Prophecy\ObjectProphecy;
use Psr\Log\LoggerInterface;
/**
- * @covers \Artemeon\HttpClient\Client\Decorator\Logger\LoggerDecorator
+ * @internal
*/
class HttpClientLogDecoratorTest extends TestCase
{
- use ProphecyTrait;
+ private LoggerInterface $logger;
+
+ private HttpClient $httpClient;
- private LoggerInterface|ObjectProphecy $logger;
- private HttpClient|ObjectProphecy $httpClient;
private LoggerDecorator $httpClientLogDecorator;
+
private ClientOptions $clientOptions;
/**
* @inheritDoc
*/
- public function setUp(): void
+ #[Override]
+ protected function setUp(): void
{
- $this->logger = $this->prophesize(LoggerInterface::class);
- $this->httpClient = $this->prophesize(HttpClient::class);
+ $this->logger = Mockery::mock(LoggerInterface::class);
+ $this->httpClient = Mockery::mock(HttpClient::class);
$this->clientOptions = ClientOptions::fromDefaults();
$this->httpClientLogDecorator = new LoggerDecorator(
- $this->httpClient->reveal(),
- $this->logger->reveal()
+ $this->httpClient,
+ $this->logger,
);
}
- /**
- * @test
- */
- public function send_WillCallDecoratedClass(): void
+ public function testSendWillCallDecoratedClass(): void
{
$request = Request::forGet(Uri::fromString('http://apache'));
$response = new Response(200, '1.1');
- $this->httpClient->send($request, $this->clientOptions)
- ->willReturn($response)
- ->shouldBeCalled();
+ $this->httpClient->shouldReceive('send')->withArgs([$request, $this->clientOptions])
+ ->once()
+ ->andReturn($response);
$result = $this->httpClientLogDecorator->send($request, $this->clientOptions);
self::assertSame($response, $result);
}
- /**
- * @test
- */
- public function send_ClientThrowsClientResponseException_ShouldBeLogged(): void
+ public function testSendClientThrowsClientResponseExceptionShouldBeLogged(): void
{
$request = Request::forGet(Uri::fromString('http://apache'));
$response = new Response(500, '1.1');
$exception = ClientResponseException::fromResponse($response, $request, 'message');
- $this->httpClient->send(Argument::any(), Argument::any())->willThrow($exception);
- $this->logger->error($exception->getMessage(), ['exception' => $exception])->shouldBeCalled();
+ $this->httpClient->shouldReceive('send')->withArgs([Mockery::any(), Mockery::any()])->andThrowExceptions([$exception]);
+ $this->logger->shouldReceive('error')->withArgs([$exception->getMessage(), ['exception' => $exception]])->once();
$this->expectException(ClientResponseException::class);
$this->httpClientLogDecorator->send($request, $this->clientOptions);
}
- /**
- * @test
- */
- public function send_ClientThrowsServerResponseException_ShouldBeLogged(): void
+ public function testSendClientThrowsServerResponseExceptionShouldBeLogged(): void
{
$request = Request::forGet(Uri::fromString('http://apache'));
$response = new Response(500, '1.1');
$exception = ServerResponseException::fromResponse($response, $request, 'message');
- $this->httpClient->send(Argument::any(), Argument::any())->willThrow($exception);
- $this->logger->error($exception->getMessage(), ['exception' => $exception])->shouldBeCalled();
+ $this->httpClient->shouldReceive('send')->withArgs([Mockery::any(), Mockery::any()])->andThrowExceptions([$exception]);
+ $this->logger->shouldReceive('error')->withArgs([$exception->getMessage(), ['exception' => $exception]])->once();
$this->expectException(ServerResponseException::class);
$this->httpClientLogDecorator->send($request, $this->clientOptions);
}
- /**
- * @test
- */
- public function send_ClientThrowsHttpClientException_ShouldBeLogged(): void
+ public function testSendClientThrowsHttpClientExceptionShouldBeLogged(): void
{
$request = Request::forGet(Uri::fromString('http://apache'));
$exception = InvalidArgumentException::forAlreadyRegisteredHeaderFields('Host');
- $this->httpClient->send(Argument::any(), Argument::any())->willThrow($exception);
- $this->logger->info($exception->getMessage(), ['exception' => $exception])->shouldBeCalled();
+ $this->httpClient->shouldReceive('send')->withArgs([Mockery::any(), Mockery::any()])->andThrowExceptions([$exception]);
+ $this->logger->shouldReceive('info')->withArgs([$exception->getMessage(), ['exception' => $exception]])->once();
$this->expectException(HttpClientException::class);
$this->httpClientLogDecorator->send($request, $this->clientOptions);
diff --git a/tests/Unit/Http/Body/Encoder/FormUrlEncoderTest.php b/tests/Unit/Http/Body/Encoder/FormUrlEncoderTest.php
index d6afdf4..ab3cc8c 100644
--- a/tests/Unit/Http/Body/Encoder/FormUrlEncoderTest.php
+++ b/tests/Unit/Http/Body/Encoder/FormUrlEncoderTest.php
@@ -20,14 +20,11 @@
use function http_build_query;
/**
- * @covers \Artemeon\HttpClient\Http\Body\Encoder\FormUrlEncoder
+ * @internal
*/
class FormUrlEncoderTest extends TestCase
{
- /**
- * @test
- */
- public function encode_ReturnsExpectedString(): void
+ public function testEncodeReturnsExpectedString(): void
{
$values = ['user' => 'Ernst Müller'];
$encoder = FormUrlEncoder::fromArray($values);
diff --git a/tests/Unit/Http/Body/Encoder/JsonEncoderTest.php b/tests/Unit/Http/Body/Encoder/JsonEncoderTest.php
index e14f501..e4d70df 100644
--- a/tests/Unit/Http/Body/Encoder/JsonEncoderTest.php
+++ b/tests/Unit/Http/Body/Encoder/JsonEncoderTest.php
@@ -16,42 +16,34 @@
use Artemeon\HttpClient\Exception\RuntimeException;
use Artemeon\HttpClient\Http\Body\Encoder\JsonEncoder;
use Artemeon\HttpClient\Http\MediaType;
-use phpmock\prophecy\PHPProphet;
+use Mockery;
use PHPUnit\Framework\TestCase;
-use Prophecy\Argument;
use stdClass;
/**
- * @covers \Artemeon\HttpClient\Http\Body\Encoder\JsonEncoder
+ * @internal
*/
class JsonEncoderTest extends TestCase
{
- /**
- * @test
- * @runInSeparateProcess
- */
- public function fromArray_JsonEncodeFailsThrows_Exception()
+ public function tearDown(): void
{
- $globalProphet = new PHPProphet();
- $globalProphecy = $globalProphet->prophesize("\Artemeon\HttpClient\Http\Body\Encoder");
+ Mockery::close(); // Mockery aufräumen
+ }
+
+ public function testFromArrayJsonEncodeFailsThrowsException(): void
+ {
+ $mockJsonEncode = Mockery::mock('overload:json_encode');
+ $mockJsonEncode->shouldReceive('__invoke')->andReturn(false);
- $globalProphecy->json_encode(Argument::any(), Argument::any())->willReturn(false);
- $globalProphecy->reveal();
+ $encoder = JsonEncoder::fromArray(['invalid' => "\xB1\x31"], 0);
$this->expectException(RuntimeException::class);
- $value = ['test' => 12];
- $options = 0;
+ $this->expectExceptionMessage("Can't encode to json: Malformed UTF-8 characters, possibly incorrectly encoded");
- $encoder = JsonEncoder::fromArray($value, $options);
$encoder->encode();
-
- $globalProphet->checkPredictions();
}
- /**
- * @test
- */
- public function fromObject_ReturnExpectedValue(): void
+ public function testFromObjectReturnExpectedValue(): void
{
$class = new stdClass();
$class->name = 'name';
@@ -63,16 +55,13 @@ public function fromObject_ReturnExpectedValue(): void
self::assertSame(MediaType::JSON, $encoder->getMimeType());
}
- /**
- * @test
- */
- public function fromArray_ReturnExpectedValue(): void
+ public function testFromArrayReturnExpectedValue(): void
{
$encoder = JsonEncoder::fromArray(
[
'name' => 'name',
- 'test' => 1
- ]
+ 'test' => 1,
+ ],
);
self::assertSame('{"name":"name","test":1}', $encoder->encode()->__toString());
diff --git a/tests/Unit/Http/Header/HeaderTest.php b/tests/Unit/Http/Header/HeaderTest.php
index 8a0dba2..03c8a46 100644
--- a/tests/Unit/Http/Header/HeaderTest.php
+++ b/tests/Unit/Http/Header/HeaderTest.php
@@ -19,52 +19,37 @@
use PHPUnit\Framework\TestCase;
/**
- * @covers \Artemeon\HttpClient\Http\Header\Header
+ * @internal
*/
class HeaderTest extends TestCase
{
- /**
- * @test
- */
- public function getValue_ReturnStringWithoutComma(): void
+ public function testGetValueReturnStringWithoutComma(): void
{
$header = Header::fromString(HeaderField::REFERER, 'some-referer');
self::assertSame('some-referer', $header->getValue());
}
- /**
- * @test
- */
- public function getValue_ReturnCommaSeparatedString(): void
+ public function testGetValueReturnCommaSeparatedString(): void
{
$header = Header::fromArray(HeaderField::REFERER, ['some-referer', 'more-stuff']);
self::assertSame('some-referer, more-stuff', $header->getValue());
}
- /**
- * @test
- */
- public function addValue_AddToArray(): void
+ public function testAddValueAddToArray(): void
{
$header = Header::fromString(HeaderField::REFERER, 'some-referer');
$header->addValue('added-string');
self::assertSame('some-referer, added-string', $header->getValue());
}
- /**
- * @test
- */
- public function getValues_ReturnsExceptedArray(): void
+ public function testGetValuesReturnsExceptedArray(): void
{
$header = Header::fromArray(HeaderField::REFERER, ['some-referer', 'more-stuff']);
self::assertSame('some-referer', $header->getValues()[0]);
self::assertSame('more-stuff', $header->getValues()[1]);
}
- /**
- * @test
- */
- public function getFieldName_ReturnsExpectedValue(): void
+ public function testGetFieldNameReturnsExpectedValue(): void
{
$header = Header::fromField(UserAgent::fromString());
self::assertSame(HeaderField::USER_AGENT, $header->getFieldName());
diff --git a/tests/Unit/Http/Header/HeadersTest.php b/tests/Unit/Http/Header/HeadersTest.php
index b22b943..6e1865b 100644
--- a/tests/Unit/Http/Header/HeadersTest.php
+++ b/tests/Unit/Http/Header/HeadersTest.php
@@ -21,25 +21,24 @@
use Artemeon\HttpClient\Http\Header\HeaderField;
use Artemeon\HttpClient\Http\Header\Headers;
use Artemeon\HttpClient\Http\Uri;
+use Override;
use PHPUnit\Framework\TestCase;
/**
- * @covers \Artemeon\HttpClient\Http\Header\Headers
+ * @internal
*/
class HeadersTest extends TestCase
{
/** @var Headers */
private $headers;
+ #[Override]
public function setUp(): void
{
$this->headers = Headers::create();
}
- /**
- * @test
- */
- public function fromFields_CreatesValidHeaders(): void
+ public function testFromFieldsCreatesValidHeaders(): void
{
$this->headers = Headers::fromFields([UserAgent::fromString('test')]);
$userAgent = $this->headers->get(HeaderField::USER_AGENT);
@@ -49,37 +48,25 @@ public function fromFields_CreatesValidHeaders(): void
self::assertSame('test', $userAgent->getValue());
}
- /**
- * @test
- */
- public function has_IsCaseIncentive_ReturnsTrue(): void
+ public function testHasIsCaseIncentiveReturnsTrue(): void
{
$this->headers->add(Header::fromField(UserAgent::fromString()));
self::assertTrue($this->headers->has('USER-AGENT'));
}
- /**
- * @test
- */
- public function has_NotExists_ReturnsFalse(): void
+ public function testHasNotExistsReturnsFalse(): void
{
$this->headers->add(Header::fromField(UserAgent::fromString()));
self::assertFalse($this->headers->has('not-exists'));
}
- /**
- * @test
- */
- public function get_NotExists_ThrowsException(): void
+ public function testGetNotExistsThrowsException(): void
{
$this->expectException(InvalidArgumentException::class);
$this->headers->get('not-exists');
}
- /**
- * @test
- */
- public function get_Exists_ReturnsValue(): void
+ public function testGetExistsReturnsValue(): void
{
$expected = Header::fromField(Authorization::forAuthBasic('john.doe', 'geheim'));
$this->headers->add($expected);
@@ -87,10 +74,7 @@ public function get_Exists_ReturnsValue(): void
self::assertSame($expected, $this->headers->get(HeaderField::AUTHORIZATION));
}
- /**
- * @test
- */
- public function get_ExistsCaseIncentive_ReturnsValue(): void
+ public function testGetExistsCaseIncentiveReturnsValue(): void
{
$expected = Header::fromField(Authorization::forAuthBasic('john.doe', 'geheim'));
$this->headers->add($expected);
@@ -98,10 +82,7 @@ public function get_ExistsCaseIncentive_ReturnsValue(): void
self::assertSame($expected, $this->headers->get('AUTHORIZATION'));
}
- /**
- * @test
- */
- public function add_Exists_ThrowsException(): void
+ public function testAddExistsThrowsException(): void
{
$header = Header::fromField(Authorization::forAuthBasic('john.doe', 'geheim'));
@@ -110,10 +91,7 @@ public function add_Exists_ThrowsException(): void
$this->headers->add($header);
}
- /**
- * @test
- */
- public function add_IsHostHeader_ShouldBeFirstHeader(): void
+ public function testAddIsHostHeaderShouldBeFirstHeader(): void
{
$AuthHeader = Header::fromField(Authorization::forAuthBasic('john.doe', 'geheim'));
$hostHeader = Header::fromField(Host::fromUri(Uri::fromString('ftp://www.artemeon.de')));
@@ -124,10 +102,7 @@ public function add_IsHostHeader_ShouldBeFirstHeader(): void
self::assertSame($hostHeader, $this->headers->getIterator()->current());
}
- /**
- * @test
- */
- public function replace_CaseIncentive_ReplaceHeader(): void
+ public function testReplaceCaseIncentiveReplaceHeader(): void
{
$AuthHeader = Header::fromField(Authorization::forAuthBasic('john.doe', 'geheim'));
$hostHeader = Header::fromField(Host::fromUri(Uri::fromString('ftp://www.artemeon.de')));
@@ -142,10 +117,7 @@ public function replace_CaseIncentive_ReplaceHeader(): void
self::assertSame($newHHostHeader, $this->headers->get(HeaderField::HOST));
}
- /**
- * @test
- */
- public function replace_IsNotExistentHostHeader_ReplaceAsFirstHeader(): void
+ public function testReplaceIsNotExistentHostHeaderReplaceAsFirstHeader(): void
{
$AuthHeader = Header::fromField(Authorization::forAuthBasic('john.doe', 'geheim'));
$hostHeader = Header::fromField(UserAgent::fromString());
@@ -161,10 +133,7 @@ public function replace_IsNotExistentHostHeader_ReplaceAsFirstHeader(): void
self::assertSame($newHHostHeader, $this->headers->getIterator()->current());
}
- /**
- * @test
- */
- public function isEmpty_FieldExists_ReturnsTrue(): void
+ public function testIsEmptyFieldExistsReturnsTrue(): void
{
$expected = Header::fromString(HeaderField::AUTHORIZATION, '');
$this->headers->add($expected);
@@ -172,10 +141,7 @@ public function isEmpty_FieldExists_ReturnsTrue(): void
self::assertTrue($this->headers->isEmpty(HeaderField::AUTHORIZATION));
}
- /**
- * @test
- */
- public function isEmpty_FieldDoesNotExists_ReturnsTrue(): void
+ public function testIsEmptyFieldDoesNotExistsReturnsTrue(): void
{
$expected = Header::fromString(HeaderField::AUTHORIZATION, 'some-credentials');
$this->headers->add($expected);
@@ -183,11 +149,7 @@ public function isEmpty_FieldDoesNotExists_ReturnsTrue(): void
self::assertTrue($this->headers->isEmpty('does-not-exists'));
}
-
- /**
- * @test
- */
- public function isEmpty_FieldExistsCaseIncentive_ReturnsTrue(): void
+ public function testIsEmptyFieldExistsCaseIncentiveReturnsTrue(): void
{
$expected = Header::fromString('Authorization', '');
$this->headers->add($expected);
@@ -195,46 +157,34 @@ public function isEmpty_FieldExistsCaseIncentive_ReturnsTrue(): void
self::assertTrue($this->headers->isEmpty('AUTHoriZATION'));
}
- /**
- * @test
- */
- public function remove_FieldDoesNotExists_DoesNothing(): void
+ public function testRemoveFieldDoesNotExistsDoesNothing(): void
{
$expected = Header::fromField(UserAgent::fromString());
$this->headers->add($expected);
$this->headers->remove('does-not-exists');
- self::assertCount(1 , $this->headers);
+ self::assertCount(1, $this->headers);
}
- /**
- * @test
- */
- public function remove_FieldExists_RemovesField(): void
+ public function testRemoveFieldExistsRemovesField(): void
{
$expected = Header::fromField(UserAgent::fromString());
$this->headers->add($expected);
$this->headers->remove(HeaderField::USER_AGENT);
- self::assertCount(0 , $this->headers);
+ self::assertCount(0, $this->headers);
}
- /**
- * @test
- */
- public function remove_FieldExistsCaseIncentive_RemovesField(): void
+ public function testRemoveFieldExistsCaseIncentiveRemovesField(): void
{
$expected = Header::fromField(UserAgent::fromString());
$this->headers->add($expected);
$this->headers->remove('USER-AGENT');
- self::assertCount(0 , $this->headers);
+ self::assertCount(0, $this->headers);
}
- /**
- * @test
- */
- public function getIterator_ReturnsArrayIterator(): void
+ public function testGetIteratorReturnsArrayIterator(): void
{
$expected = Header::fromField(Authorization::forAuthBasic('john.doe', 'geheim'));
$this->headers->add($expected);
diff --git a/tests/Unit/Http/RequestTest.php b/tests/Unit/Http/RequestTest.php
index c2193b9..493764a 100644
--- a/tests/Unit/Http/RequestTest.php
+++ b/tests/Unit/Http/RequestTest.php
@@ -25,15 +25,11 @@
use Psr\Http\Message\StreamInterface;
/**
- * @covers \Artemeon\HttpClient\Http\Request
- * @covers \Artemeon\HttpClient\Http\Message
+ * @internal
*/
class RequestTest extends TestCase
{
- /**
- * @test
- */
- public function forOptions_SetValidRequestMethod(): void
+ public function testForOptionsSetValidRequestMethod(): void
{
$expectedUrl = Uri::fromString('http://apache/endpoints/upload.php');
$expectedProtocol = '1.1';
@@ -41,7 +37,7 @@ public function forOptions_SetValidRequestMethod(): void
$request = Request::forOptions(
$expectedUrl,
null,
- $expectedProtocol
+ $expectedProtocol,
);
self::assertSame(Request::METHOD_OPTIONS, $request->getMethod());
@@ -49,19 +45,16 @@ public function forOptions_SetValidRequestMethod(): void
self::assertSame($expectedProtocol, $request->getProtocolVersion());
}
- /**
- * @test
- */
- public function forPost_SetValidRequestMethod(): void
+ public function testForPostSetValidRequestMethod(): void
{
$expectedUrl = Uri::fromString('http://apache/endpoints/upload.php');
$expectedProtocol = '2.0';
$request = Request::forPost(
$expectedUrl,
- Body::fromEncoder(FormUrlEncoder::fromArray(["username" => 'john.doe'])),
+ Body::fromEncoder(FormUrlEncoder::fromArray(['username' => 'john.doe'])),
null,
- $expectedProtocol
+ $expectedProtocol,
);
self::assertSame(Request::METHOD_POST, $request->getMethod());
@@ -69,30 +62,24 @@ public function forPost_SetValidRequestMethod(): void
self::assertSame($expectedProtocol, $request->getProtocolVersion());
}
- /**
- * @test
- */
- public function forPost_WillCreateAndAddContentHeader(): void
+ public function testForPostWillCreateAndAddContentHeader(): void
{
$request = Request::forPost(
Uri::fromString('http://apache/endpoints/upload.php'),
- Body::fromEncoder(FormUrlEncoder::fromArray(["username" => 'john.doe'])),
- null // Test: Headers is null, Request must create Headers collection an add headers from body
+ Body::fromEncoder(FormUrlEncoder::fromArray(['username' => 'john.doe'])),
+ null, // Test: Headers is null, Request must create Headers collection an add headers from body
);
self::assertSame(MediaType::FORM_URL_ENCODED, $request->getHeaderLine(HeaderField::CONTENT_TYPE));
self::assertSame(17, (int) $request->getHeaderLine(HeaderField::CONTENT_LENGTH));
}
- /**
- * @test
- */
- public function forPost_WillAddContentHeader(): void
+ public function testForPostWillAddContentHeader(): void
{
$request = Request::forPost(
Uri::fromString('http://apache/endpoints/upload.php'),
- Body::fromEncoder(FormUrlEncoder::fromArray(["username" => 'john.doe'])),
- Headers::fromFields([UserAgent::fromString('test')]) // Test: Add header from body to given collection
+ Body::fromEncoder(FormUrlEncoder::fromArray(['username' => 'john.doe'])),
+ Headers::fromFields([UserAgent::fromString('test')]), // Test: Add header from body to given collection
);
self::assertSame(MediaType::FORM_URL_ENCODED, $request->getHeaderLine(HeaderField::CONTENT_TYPE));
@@ -100,10 +87,7 @@ public function forPost_WillAddContentHeader(): void
self::assertSame('test', $request->getHeaderLine(HeaderField::USER_AGENT));
}
- /**
- * @test
- */
- public function forDelete_SetValidRequestMethod(): void
+ public function testForDeleteSetValidRequestMethod(): void
{
$expectedUrl = Uri::fromString('http://apache/endpoints/upload.php');
$expectedProtocol = '1.1';
@@ -111,7 +95,7 @@ public function forDelete_SetValidRequestMethod(): void
$request = Request::forDelete(
$expectedUrl,
null,
- $expectedProtocol
+ $expectedProtocol,
);
self::assertSame(Request::METHOD_DELETE, $request->getMethod());
@@ -119,10 +103,7 @@ public function forDelete_SetValidRequestMethod(): void
self::assertSame($expectedProtocol, $request->getProtocolVersion());
}
- /**
- * @test
- */
- public function forGet_SetValidRequestMethod(): void
+ public function testForGetSetValidRequestMethod(): void
{
$expectedUrl = Uri::fromString('http://apache/endpoints/upload.php');
$expectedProtocol = '1.1';
@@ -130,7 +111,7 @@ public function forGet_SetValidRequestMethod(): void
$request = Request::forGet(
$expectedUrl,
null,
- $expectedProtocol
+ $expectedProtocol,
);
self::assertSame(Request::METHOD_GET, $request->getMethod());
@@ -138,10 +119,7 @@ public function forGet_SetValidRequestMethod(): void
self::assertSame($expectedProtocol, $request->getProtocolVersion());
}
- /**
- * @test
- */
- public function forGet_UrlWillCreateAndSetHostHeader(): void
+ public function testForGetUrlWillCreateAndSetHostHeader(): void
{
$expectedUrl = Uri::fromString('http://artemeon.de/endpoints/upload.php');
$request = Request::forGet($expectedUrl);
@@ -149,10 +127,7 @@ public function forGet_UrlWillCreateAndSetHostHeader(): void
self::assertSame('artemeon.de', $request->getHeaderLine(HeaderField::HOST));
}
- /**
- * @test
- */
- public function forGet_UrlWillAddHostHeader(): void
+ public function testForGetUrlWillAddHostHeader(): void
{
$expectedUrl = Uri::fromString('http://artemeon.de/endpoints/upload.php');
$request = Request::forGet($expectedUrl, Headers::fromFields([UserAgent::fromString()]));
@@ -160,19 +135,16 @@ public function forGet_UrlWillAddHostHeader(): void
self::assertSame('artemeon.de', $request->getHeaderLine(HeaderField::HOST));
}
- /**
- * @test
- */
- public function forPut_SetValidRequestMethod(): void
+ public function testForPutSetValidRequestMethod(): void
{
$expectedUrl = Uri::fromString('http://apache/endpoints/upload.php');
$expectedProtocol = '1.1';
$request = Request::forPut(
$expectedUrl,
- Body::fromEncoder(FormUrlEncoder::fromArray(["username" => 'john.doe'])),
+ Body::fromEncoder(FormUrlEncoder::fromArray(['username' => 'john.doe'])),
null,
- $expectedProtocol
+ $expectedProtocol,
);
self::assertSame(Request::METHOD_PUT, $request->getMethod());
@@ -180,19 +152,16 @@ public function forPut_SetValidRequestMethod(): void
self::assertSame($expectedProtocol, $request->getProtocolVersion());
}
- /**
- * @test
- */
- public function forPatch_SetValidRequestMethod(): void
+ public function testForPatchSetValidRequestMethod(): void
{
$expectedUrl = Uri::fromString('http://apache/endpoints/upload.php');
$expectedProtocol = '1.1';
$request = Request::forPatch(
$expectedUrl,
- Body::fromEncoder(FormUrlEncoder::fromArray(["username" => 'john.doe'])),
+ Body::fromEncoder(FormUrlEncoder::fromArray(['username' => 'john.doe'])),
null,
- $expectedProtocol
+ $expectedProtocol,
);
self::assertSame(Request::METHOD_PATCH, $request->getMethod());
@@ -200,10 +169,7 @@ public function forPatch_SetValidRequestMethod(): void
self::assertSame($expectedProtocol, $request->getProtocolVersion());
}
- /**
- * @test
- */
- public function getBody_BodyIsNull_WillReturnEmptyStreamObject(): void
+ public function testGetBodyBodyIsNullWillReturnEmptyStreamObject(): void
{
$request = Request::forGet(Uri::fromString('http://artemeon.de/endpoints/upload.php'));
@@ -211,78 +177,54 @@ public function getBody_BodyIsNull_WillReturnEmptyStreamObject(): void
self::assertEmpty($request->getBody()->__toString());
}
- /**
- * @test
- */
- public function getBody_BodyIsSet_WillReturnStreamObject(): void
+ public function testGetBodyBodyIsSetWillReturnStreamObject(): void
{
$request = Request::forPost(
Uri::fromString('http://artemeon.de/endpoints/upload.php'),
- Body::fromString(MediaType::UNKNOWN, 'test')
+ Body::fromString(MediaType::UNKNOWN, 'test'),
);
self::assertInstanceOf(StreamInterface::class, $request->getBody());
self::assertSame('test', $request->getBody()->__toString());
}
- /**
- * @test
- */
- public function hasHeader_ReturnsTrue(): void
+ public function testHasHeaderReturnsTrue(): void
{
$request = Request::forGet(Uri::fromString('http://artemeon.de/endpoints/upload.php'));
self::assertTrue($request->hasHeader(HeaderField::HOST));
}
- /**
- * @test
- */
- public function hasHeader_ReturnsFalse(): void
+ public function testHasHeaderReturnsFalse(): void
{
$request = Request::forGet(Uri::fromString('http://artemeon.de/endpoints/upload.php'));
self::assertFalse($request->hasHeader('nit_exists'));
}
- /**
- * @test
- */
- public function getHeader_NotExists_ReturnsEmptyArray(): void
+ public function testGetHeaderNotExistsReturnsEmptyArray(): void
{
$request = Request::forGet(Uri::fromString('http://artemeon.de/endpoints/upload.php'));
self::assertSame([], $request->getHeader('not_exists'));
}
- /**
- * @test
- */
- public function getHeader_Exists_ReturnsValidArray(): void
+ public function testGetHeaderExistsReturnsValidArray(): void
{
$request = Request::forGet(Uri::fromString('http://artemeon.de/endpoints/upload.php'));
self::assertSame(['artemeon.de'], $request->getHeader(HeaderField::HOST));
}
- /**
- * @test
- */
- public function getHeaderLine_NotExists_ReturnsEmptyString(): void
+ public function testGetHeaderLineNotExistsReturnsEmptyString(): void
{
$request = Request::forGet(Uri::fromString('http://artemeon.de/endpoints/upload.php'));
self::assertSame('', $request->getHeaderLine('not_exists'));
}
- /**
- * @test
- */
- public function getHeaderLine_Exists_ReturnsValidString(): void
+ public function testGetHeaderLineExistsReturnsValidString(): void
{
$request = Request::forGet(Uri::fromString('http://www.artemeon.de/endpoints/upload.php'));
self::assertSame('www.artemeon.de', $request->getHeaderLine(HeaderField::HOST));
}
- /**
- * @test
- */
- public function getHeaders_ReturnsValidArray(): void
+ public function testGetHeadersReturnsValidArray(): void
{
$request = Request::forGet(Uri::fromString('http://artemeon.de/endpoints/upload.php'));
@@ -291,30 +233,21 @@ public function getHeaders_ReturnsValidArray(): void
self::assertSame([HeaderField::HOST => ['artemeon.de']], $request->getHeaders());
}
- /**
- * @test
- */
- public function getRequestTarget__WithoutPath_ReturnsSlash(): void
+ public function testGetRequestTargetWithoutPathReturnsSlash(): void
{
$request = Request::forGet(Uri::fromString('http://www.artemeon.de'));
- self::assertSame('/' , $request->getRequestTarget());
+ self::assertSame('/', $request->getRequestTarget());
}
- /**
- * @test
- */
- public function getRequestTarget__WithPath_ReturnsPath(): void
+ public function testGetRequestTargetWithPathReturnsPath(): void
{
$request = Request::forGet(Uri::fromString('http://www.artemeon.de/some/Path/index.html'));
- self::assertSame('/some/Path/index.html' , $request->getRequestTarget());
+ self::assertSame('/some/Path/index.html', $request->getRequestTarget());
}
- /**
- * @test
- */
- public function getRequestTarget__WithPathAndQuery_ReturnsPathAnsQuery(): void
+ public function testGetRequestTargetWithPathAndQueryReturnsPathAnsQuery(): void
{
$request = Request::forGet(Uri::fromString('http://www.artemeon.de/index.html?User=john.doe'));
- self::assertSame('/index.html?User=john.doe' , $request->getRequestTarget());
+ self::assertSame('/index.html?User=john.doe', $request->getRequestTarget());
}
}
diff --git a/tests/Unit/Http/ResponseTest.php b/tests/Unit/Http/ResponseTest.php
index 6608638..87e28fb 100644
--- a/tests/Unit/Http/ResponseTest.php
+++ b/tests/Unit/Http/ResponseTest.php
@@ -20,20 +20,17 @@
use PHPUnit\Framework\TestCase;
/**
- * @covers \Artemeon\HttpClient\Http\Response
+ * @internal
*/
class ResponseTest extends TestCase
{
- /**
- * @test
- */
- public function getStatusCode_ReturnValidCode()
+ public function testGetStatusCodeReturnValidCode(): void
{
$response = new Response(
200,
'1.1',
Stream::fromString('test'),
- Headers::fromFields([UserAgent::fromString()])
+ Headers::fromFields([UserAgent::fromString()]),
);
self::assertSame(200, $response->getStatusCode());
diff --git a/tests/Unit/Http/UriTest.php b/tests/Unit/Http/UriTest.php
index 0e24f81..bccf365 100644
--- a/tests/Unit/Http/UriTest.php
+++ b/tests/Unit/Http/UriTest.php
@@ -13,146 +13,96 @@
namespace Artemeon\HttpClient\Tests\Unit\Http;
-use Artemeon\HttpClient\Exception\InvalidArgumentException;
use Artemeon\HttpClient\Http\Uri;
use PHPUnit\Framework\TestCase;
+/**
+ * @internal
+ */
class UriTest extends TestCase
{
- /**
- * @test
- */
- public function fromString_SetValidValues(): void
+ public function testFromStringSetValidValues(): void
{
$expected = 'http://www.artemeon.de';
$url = Uri::fromString($expected);
self::assertSame($expected, $url->__toString());
}
- /**
- * @test
- */
- public function getQuery_ReturnExpectedValue(): void
+ public function testGetQueryReturnExpectedValue(): void
{
$expected = 'user=john.doe';
$url = Uri::fromQueryParams('http://www.artemeon.de', ['user' => 'john.doe']);
self::assertSame($expected, $url->getQuery());
}
- /**
- * @test
- */
- public function getFragment_ReturnExpectedValue(): void
+ public function testGetFragmentReturnExpectedValue(): void
{
$expected = 'anker';
$url = Uri::fromString('http://www.artemeon.de/pfad/test.html#' . $expected);
self::assertSame($expected, $url->getFragment());
}
- /**
- * @test
- */
- public function getFragment_ReturnsEmptyString(): void
+ public function testGetFragmentReturnsEmptyString(): void
{
$url = Uri::fromString('http://www.artemeon.de/pfad/test.html');
self::assertSame('', $url->getFragment());
}
- /**
- * @test
- */
- public function getUserInfo_ReturnUserPassword(): void
+ public function testGetUserInfoReturnUserPassword(): void
{
$url = Uri::fromString('https://dsi:topsecret@www.artemeon.de');
self::assertSame('dsi:topsecret', $url->getUserInfo());
}
- /**
- * @test
- */
- public function getUserInfo_ReturnOnlyUser(): void
+ public function testGetUserInfoReturnOnlyUser(): void
{
$url = Uri::fromString('https://dsi@www.artemeon.de');
self::assertSame('dsi', $url->getUserInfo());
}
- /**
- * @test
- */
- public function getUserInfo_ReturnEmptyString(): void
+ public function testGetUserInfoReturnEmptyString(): void
{
$url = Uri::fromString('https://www.artemeon.de');
self::assertSame('', $url->getUserInfo());
}
- /**
- * @test
- */
- public function getScheme_ReturnExpectedValue(): void
+ public function testGetSchemeReturnExpectedValue(): void
{
$url = Uri::fromString('ftp://dsi:topsecret@www.artemeon.de');
self::assertSame('ftp', $url->getScheme());
}
- /**
- * @test
- */
- public function getHost_ReturnExpectedValue(): void
+ public function testGetHostReturnExpectedValue(): void
{
$url = Uri::fromString('http://www.artemeon.de:8080/path/to/file.html');
self::assertSame('www.artemeon.de', $url->getHost());
}
- /**
- * @test
- */
- public function getPort_ReturnExpectedNull(): void
+ public function testGetPortReturnExpectedNull(): void
{
$url = Uri::fromString('http://www.artemeon.de/path/to/file.html');
self::assertNull($url->getPort());
}
- /**
- * @test
- */
- public function getPort_ReturnExpectedInt(): void
+ public function testGetPortReturnExpectedInt(): void
{
$url = Uri::fromString('http://www.artemeon.de:8080/path/to/file.html');
self::assertSame(8080, $url->getPort());
}
- /**
- * @test
- */
- public function getPath_ReturnExpectedString(): void
+ public function testGetPathReturnExpectedString(): void
{
$url = Uri::fromString('http://www.artemeon.de:8080/path/to/file.html');
self::assertSame('/path/to/file.html', $url->getPath());
}
- /**
- * @test
- */
- public function getPath_ReturnExpectedEmptyString(): void
+ public function testGetPathReturnExpectedEmptyString(): void
{
$url = Uri::fromString('http://www.artemeon.de:8080');
self::assertSame('', $url->getPath());
}
- /**
- * @test
- */
- public function withScheme_IsNotString_ThroesException(): void
- {
- $this->expectException(InvalidArgumentException::class);
- $url = Uri::fromString('http://www.artemeon.de:8080');
- $url->withScheme(0);
- }
-
- /**
- * @test
- */
- public function withScheme_ReturnsUpdatedInstance(): void
+ public function testWithSchemeReturnsUpdatedInstance(): void
{
$url = Uri::fromString('http://www.artemeon.de:8080');
$cloned = $url->withScheme('FTP');
@@ -162,10 +112,7 @@ public function withScheme_ReturnsUpdatedInstance(): void
self::assertSame('ftp://www.artemeon.de:8080', $cloned->__toString());
}
- /**
- * @test
- */
- public function withUserInfo_EmptyUserString_RemovesUserData(): void
+ public function testWithUserInfoEmptyUserStringRemovesUserData(): void
{
$url = Uri::fromString('http://dietmar.simons:password@www.artemeon.de:8080');
$cloned = $url->withUserInfo('');
@@ -175,10 +122,7 @@ public function withUserInfo_EmptyUserString_RemovesUserData(): void
self::assertEmpty($cloned->getUserInfo());
}
- /**
- * @test
- */
- public function withUserInfo_WithUserString_SetsValidUserInfo(): void
+ public function testWithUserInfoWithUserStringSetsValidUserInfo(): void
{
$url = Uri::fromString('http://dietmar.simons:password@www.artemeon.de');
$cloned = $url->withUserInfo('user');
@@ -188,10 +132,7 @@ public function withUserInfo_WithUserString_SetsValidUserInfo(): void
self::assertSame('user', $cloned->getUserInfo());
}
- /**
- * @test
- */
- public function withUserInfo_WithUserStringAndPassword_SetsValidUserInfo(): void
+ public function testWithUserInfoWithUserStringAndPasswordSetsValidUserInfo(): void
{
$url = Uri::fromString('http://dietmar.simons:password@www.artemeon.de');
$cloned = $url->withUserInfo('user', 'password');
@@ -201,21 +142,7 @@ public function withUserInfo_WithUserStringAndPassword_SetsValidUserInfo(): void
self::assertSame('user:password', $cloned->getUserInfo());
}
- /**
- * @test
- */
- public function withHost_IsNotString_ThrowsException(): void
- {
- $url = Uri::fromString('http://dietmar.simons:password@www.artemeon.de');
- $this->expectException(InvalidArgumentException::class);
-
- $url->withHost(123);
- }
-
- /**
- * @test
- */
- public function withHost_IsUpperCase_WillConvertedToLoweCase(): void
+ public function testWithHostIsUpperCaseWillConvertedToLoweCase(): void
{
$url = Uri::fromString('http://www.artemeon.de');
$cloned = $url->withHost('ARTEMEON.COM');
diff --git a/tests/Unit/Stream/StreamTest.php b/tests/Unit/Stream/StreamTest.php
index ee8ef4b..67846ee 100644
--- a/tests/Unit/Stream/StreamTest.php
+++ b/tests/Unit/Stream/StreamTest.php
@@ -15,131 +15,162 @@
use Artemeon\HttpClient\Exception\RuntimeException;
use Artemeon\HttpClient\Stream\Stream;
+use Mockery;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamDirectory;
-use phpmock\prophecy\PHPProphet;
+use Override;
+use php_user_filter;
+use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
-use Prophecy\Argument;
-use Prophecy\Prophecy\ProphecyInterface;
+class FalseReturningFilter extends php_user_filter
+{
+ public function filter($in, $out, &$consumed, $closing): int
+ {
+ return PSFS_ERR_FATAL;
+ }
+}
+
+class FalseReturningStream
+{
+ public $context;
+
+ public function stream_open($path, $mode, $options, &$opened_path)
+ {
+ return true;
+ }
+
+ public function stream_stat(): array | false
+ {
+ return false;
+ }
+
+ public function stream_read($count)
+ {
+ return false;
+ }
+
+ public function stream_write($data)
+ {
+ return false;
+ }
+
+ public function stream_eof()
+ {
+ return true;
+ }
+
+ public function stream_get_contents($resource): false | string
+ {
+ return false;
+ }
+}
/**
- * @covers \Artemeon\HttpClient\Stream\Stream
+ * @internal
*/
class StreamTest extends TestCase
{
- /** @var Stream */
- private $stream;
-
- /** @var ProphecyInterface */
- private $globalProphecy;
+ private Stream $stream;
- /** @var PHPProphet */
- private $globalProphet;
-
- /** @var vfsStreamDirectory */
- private $filesystem;
+ private vfsStreamDirectory $filesystem;
/**
* @inheritDoc
*/
- public function setUp(): void
+ #[Override]
+ protected function setUp(): void
{
- $this->globalProphet = new PHPProphet();
- $this->globalProphecy = $this->globalProphet->prophesize('\Artemeon\HttpClient\Stream');
+ parent::setUp();
+
$this->filesystem = vfsStream::setup('stream');
+ file_put_contents($this->filesystem->url() . '/generated.json', '');
vfsStream::copyFromFileSystem(
__DIR__ . '/../../Fixtures/encoder',
- $this->filesystem
+ $this->filesystem,
);
}
/**
- * @inheritdoc
+ * @inheritDoc
*/
- public function tearDown(): void
+ #[Override]
+ protected function tearDown(): void
{
- $this->globalProphet->checkPredictions();
- $this->stream = null;
+ Mockery::close();
+ parent::tearDown();
}
- /**
- * @test
- * @runInSeparateProcess
- */
- public function __construct_ResourceIsInvalid_ThrowsException()
+ public function constructResourceIsInvalidThrowsException(): void
{
- $this->globalProphecy->fopen(Argument::any(), Argument::any())
- ->shouldBeCalled()
- ->willReturn(false);
+ $fopenMock = Mockery::mock('overload:fopen');
+ $fopenMock->shouldReceive('__invoke')
+ ->with(Mockery::any(), Mockery::any())
+ ->andReturn(false);
- $this->globalProphecy->reveal();
$this->expectException(RuntimeException::class);
- $this->stream = Stream::fromFileMode('rw');
+ Stream::fromFileMode('rw');
}
- /**
- * @test
- */
- public function appendStream_IsDetached_ThrowsException(): void
+ public function testAppendStreamIsDetachedThrowsException(): void
{
$this->stream = Stream::fromString('test');
$this->stream->detach();
$this->expectException(RuntimeException::class);
- $this->expectErrorMessage('Stream is detached');
+ $this->expectExceptionMessage('Stream is detached');
$this->stream->appendStream(Stream::fromString('append'));
}
- /**
- * @test
- */
- public function appendStream_IsNotWriteable_ThrowsException(): void
+ public function testAppendStreamIsNotWriteableThrowsException(): void
{
$this->stream = Stream::fromFileMode('r');
$this->expectException(RuntimeException::class);
- $this->expectErrorMessage('Stream is not writeable');
+ $this->expectExceptionMessage('Stream is not writeable');
$this->stream->appendStream(Stream::fromString('append'));
}
- /**
- * @test
- */
- public function appendStream_GivenStreamIsNotReadable_ThrowsException(): void
+ public function testAppendStreamGivenStreamIsNotReadableThrowsException(): void
{
$this->stream = Stream::fromString('test');
$this->expectException(RuntimeException::class);
- $this->expectErrorMessage("Can't append not readable stream");
+ $this->expectExceptionMessage("Can't append not readable stream");
$writeOnlyStream = Stream::fromFile($this->filesystem->url() . '/generated.json', 'w');
$this->stream->appendStream($writeOnlyStream);
}
- /**
- * @test
- */
- public function appendStream_CantCopyStream_ThrowsException(): void
+ public function testAppendStreamCantCopyStreamThrowsException(): void
{
- $this->globalProphecy->stream_copy_to_stream(Argument::any(), Argument::any())
- ->shouldBeCalled()
- ->willReturn(false);
+ stream_wrapper_register('falsestream', FalseReturningStream::class);
+ $resourceMock = fopen('falsestream://test', 'w');
- $this->globalProphecy->reveal();
+ $testString = 'test';
+ $streamFeature = Stream::fromString($testString);
+
+ $streamAppendMock = $this->createMock(Stream::class);
+ $streamAppendMock->expects($this->once())
+ ->method('getResource')
+ ->willReturn($resourceMock);
+
+ $streamAppendMock->expects($this->once())
+ ->method('isReadable')
+ ->willReturn(true);
- $this->stream = Stream::fromString('test');
$this->expectException(RuntimeException::class);
- $this->expectErrorMessage("Append failed");
+ $this->expectExceptionMessage('Append failed');
- $writeOnlyStream = Stream::fromFile($this->filesystem->url() . '/generated.json');
- $this->stream->appendStream($writeOnlyStream);
+ $streamFeature->appendStream($streamAppendMock);
}
- /**
- * @test
- */
- public function appendStream_ReturnsAppendedStream(): void
+ public function mockedStreamCopyToStream($source, $destination)
+ {
+ return call_user_func_array($GLOBALS['originalStreamCopyToStream'], func_get_args());
+ }
+
+ public function testAppendStreamReturnsAppendedStream(): void
{
$this->stream = Stream::fromString('test');
$this->stream->appendStream(Stream::fromString('_appended'));
@@ -147,26 +178,21 @@ public function appendStream_ReturnsAppendedStream(): void
self::assertSame('test_appended', $this->stream->__toString());
}
- /**
- * @test
- * @runInSeparateProcess
- */
- public function fromFile_ResourceIsInvalid_ThrowsException(): void
+ public function testFromFileResourceIsInvalidThrowsException(): void
{
- $this->globalProphecy->fopen(Argument::any(), Argument::any())
- ->shouldBeCalled()
- ->willReturn(false);
+ $fopenMock = Mockery::mock('overload:fopen');
+ $fopenMock->shouldReceive('__invoke')
+ ->with(Mockery::any(), Mockery::any())
+ ->andReturn(false);
- $this->globalProphecy->reveal();
$this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Can\'t open file /does/not/exists.txt');
$this->stream = Stream::fromFile('/does/not/exists.txt');
+ $this->fail('Expected RuntimeException was not thrown.');
}
- /**
- * @test
- */
- public function __toString_IsDetached_ReturnEmptyString(): void
+ public function testToStringIsDetachedReturnEmptyString(): void
{
$this->stream = Stream::fromString('some_content');
$this->stream->detach();
@@ -175,10 +201,7 @@ public function __toString_IsDetached_ReturnEmptyString(): void
self::assertEmpty($content);
}
- /**
- * @test
- */
- public function __toString_ReturnValidString(): void
+ public function testToStringReturnValidString(): void
{
$expected = 'some_content';
$this->stream = Stream::fromString($expected);
@@ -187,10 +210,7 @@ public function __toString_ReturnValidString(): void
self::assertSame($expected, $content);
}
- /**
- * @test
- */
- public function __toString_WithBytesRead_ReturnsCompleteString(): void
+ public function testToStringWithBytesReadReturnsCompleteString(): void
{
$expected = 'some_content';
$this->stream = Stream::fromString($expected);
@@ -201,87 +221,61 @@ public function __toString_WithBytesRead_ReturnsCompleteString(): void
self::assertSame($expected, $content);
}
- /**
- * @test
- * @doesNotPerformAssertions
- * @runInSeparateProcess
- */
- public function close_IsDetached_ShouldNotCallClose(): void
+ public function testCloseIsDetachedShouldNotCallClose(): void
{
- $this->globalProphecy->fclose(Argument::type('resource'))->will(
- function ($args) {
- return fclose($args[0]);
- }
- )->shouldBeCalledTimes(1);
-
- $this->globalProphecy->reveal();
+ $fcloseMock = Mockery::mock('overload:fclose');
+ $fcloseMock->shouldReceive('__invoke')
+ ->with(Mockery::type('resource'))
+ ->andReturnUsing(static fn ($resource) => fclose($resource));
$this->stream = Stream::fromString('content');
$this->stream->detach();
$this->stream->close();
+
+ Mockery::close();
+ $this->addToAssertionCount(1);
}
- /**
- * @test
- * @doesNotPerformAssertions
- * @runInSeparateProcess
- */
- public function close_ShouldCallClose(): void
+ public function testCloseShouldCallClose(): void
{
- $this->globalProphecy->fclose(Argument::type('resource'))->will(
- function ($args) {
- return fclose($args[0]);
- }
- )->shouldBeCalled();
-
- $this->globalProphecy->reveal();
+ $fcloseMock = Mockery::mock('overload:fclose');
+ $fcloseMock->shouldReceive('__invoke')
+ ->with(Mockery::type('resource'))
+ ->once();
$this->stream = Stream::fromString('content');
$this->stream->close();
- $this->stream->eof();
+
+ Mockery::close();
+ $this->addToAssertionCount(1);
}
- /**
- * @test
- * @runInSeparateProcess
- */
- public function detach_ShouldCallClose(): void
+ public function testDetachShouldCallClose(): void
{
- $this->globalProphecy->fclose(Argument::type('resource'))->will(
- function ($args) {
- return fclose($args[0]);
- }
- )->shouldBeCalled();
+ $fcloseMock = Mockery::mock('overload:fclose');
+ $fcloseMock->shouldReceive('__invoke')
+ ->with(Mockery::type('resource'))
+ ->once();
- $this->globalProphecy->reveal();
$this->stream = Stream::fromString('content');
$this->stream->detach();
self::assertSame([], $this->stream->getMetadata());
}
- /**
- * @test
- * @runInSeparateProcess
- */
- public function getSize_ReturnExpectedValue(): void
+ public function testGetSizeReturnExpectedValue(): void
{
- $this->globalProphecy->fstat(Argument::type('resource'))->will(
- function ($args) {
- return fstat($args[0]);
- }
- )->shouldBeCalled();
+ $fstatMock = Mockery::mock('overload:fstat');
+ $fstatMock->shouldReceive('__invoke')
+ ->with(Mockery::type('resource'))
+ ->once();
- $this->globalProphecy->reveal();
$this->stream = Stream::fromString('content');
self::assertSame(7, $this->stream->getSize());
}
- /**
- * @test
- */
- public function getSize_IsDetached_ReturnNull(): void
+ public function testGetSizeIsDetachedReturnNull(): void
{
$this->stream = Stream::fromString('content');
$this->stream->detach();
@@ -289,36 +283,36 @@ public function getSize_IsDetached_ReturnNull(): void
self::assertNull($this->stream->getSize());
}
- /**
- * @test
- */
- public function tell_IsDetached_ThrowsException(): void
+ public function testTellIsDetachedThrowsException(): void
{
$this->stream = Stream::fromString('content');
$this->stream->detach();
$this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Stream is detached');
$this->stream->tell();
}
- /**
- * @test
- */
- public function tell_FtellReturnsFalse_ThrowsException(): void
+ public function testTellFtellReturnsFalseThrowsException(): void
{
- $this->globalProphecy->ftell(Argument::type('resource'))->willReturn(false);
- $this->globalProphecy->reveal();
+ // generate resource and set file pointer to not allowed position
+ $resourceMock = fopen('php://memory', 'r+');
+ fwrite($resourceMock, 'test content');
+ fseek($resourceMock, -1);
- $this->stream = Stream::fromString('content');
+ $streamHandler = $this->getMockBuilder(Stream::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getResource'])
+ ->getMock();
+
+ $streamHandler->expects($this->exactly(2))->method('getResource')->willReturn($resourceMock);
$this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Can\'t determine position');
- $this->stream->tell();
+ $streamHandler->tell();
}
- /**
- * @test
- */
- public function tell_ReturnsExpectedValued(): void
+ public function testTellReturnsExpectedValued(): void
{
$this->stream = Stream::fromString('content');
$this->stream->getContents();
@@ -326,10 +320,7 @@ public function tell_ReturnsExpectedValued(): void
self::assertSame(7, $this->stream->tell());
}
- /**
- * @test
- */
- public function eof_IsDetached_ReturnsTrue(): void
+ public function testEofIsDetachedReturnsTrue(): void
{
$this->stream = Stream::fromString('content');
$this->stream->detach();
@@ -337,10 +328,7 @@ public function eof_IsDetached_ReturnsTrue(): void
self::assertTrue($this->stream->eof());
}
- /**
- * @test
- */
- public function eof_ReturnsExpectedValued(): void
+ public function testEofReturnsExpectedValued(): void
{
$this->stream = Stream::fromString('content');
self::assertFalse($this->stream->eof());
@@ -349,10 +337,7 @@ public function eof_ReturnsExpectedValued(): void
self::assertTrue($this->stream->eof());
}
- /**
- * @test
- */
- public function isSeekable_IsDetached_ReturnFalse(): void
+ public function testIsSeekableIsDetachedReturnFalse(): void
{
$this->stream = Stream::fromString('content');
$this->stream->detach();
@@ -360,10 +345,7 @@ public function isSeekable_IsDetached_ReturnFalse(): void
self::assertFalse($this->stream->isSeekable());
}
- /**
- * @test
- */
- public function isSeekable_WithNonSeekableFileModes_ReturnFalse(): void
+ public function testIsSeekableWithNonSeekableFileModesReturnFalse(): void
{
$this->stream = Stream::fromFile($this->filesystem->url() . '/generated.json', 'a');
self::assertFalse($this->stream->isSeekable());
@@ -372,19 +354,13 @@ public function isSeekable_WithNonSeekableFileModes_ReturnFalse(): void
self::assertFalse($this->stream->isSeekable());
}
- /**
- * @test
- */
- public function isSeekable_ReturnTrue(): void
+ public function testIsSeekableReturnTrue(): void
{
$this->stream = Stream::fromFile($this->filesystem->url() . '/generated.json');
self::assertTrue($this->stream->isSeekable());
}
- /**
- * @test
- */
- public function seek_IsDetached_ThrowsException(): void
+ public function testSeekIsDetachedThrowsException(): void
{
$this->stream = Stream::fromString('content');
$this->stream->detach();
@@ -393,21 +369,15 @@ public function seek_IsDetached_ThrowsException(): void
$this->stream->seek(7);
}
- /**
- * @test
- */
- public function seek_FseekFails_ThrowsException(): void
+ public function testSeekFseekFailsThrowsException(): void
{
$this->stream = Stream::fromString('content');
$this->expectException(RuntimeException::class);
- $this->stream->seek(456);
+ $this->stream->seek(-456);
}
- /**
- * @test
- */
- public function seek_FseekSetsValidPointer(): void
+ public function testSeekFseekSetsValidPointer(): void
{
$this->stream = Stream::fromString('content');
$this->stream->seek(2);
@@ -415,10 +385,7 @@ public function seek_FseekSetsValidPointer(): void
self::assertSame(2, $this->stream->tell());
}
- /**
- * @test
- */
- public function rewind_IsDetached_ThrowsException(): void
+ public function testRewindIsDetachedThrowsException(): void
{
$this->stream = Stream::fromString('content');
$this->stream->detach();
@@ -427,10 +394,7 @@ public function rewind_IsDetached_ThrowsException(): void
$this->stream->rewind();
}
- /**
- * @test
- */
- public function rewind_IsNotSeekable_ThrowsException(): void
+ public function testRewindIsNotSeekableThrowsException(): void
{
$this->stream = Stream::fromFile($this->filesystem->url() . '/generated.json', 'a');
$this->expectException(RuntimeException::class);
@@ -438,10 +402,7 @@ public function rewind_IsNotSeekable_ThrowsException(): void
$this->stream->rewind();
}
- /**
- * @test
- */
- public function rewind_ShouldResetFilePointerToZero(): void
+ public function testRewindShouldResetFilePointerToZero(): void
{
$this->stream = Stream::fromFile($this->filesystem->url() . '/generated.json', 'r+');
$this->stream->getContents();
@@ -450,10 +411,7 @@ public function rewind_ShouldResetFilePointerToZero(): void
self::assertSame(0, $this->stream->tell());
}
- /**
- * @test
- */
- public function isWritable_IsDetached_ReturnsFalse(): void
+ public function testIsWritableIsDetachedReturnsFalse(): void
{
$this->stream = Stream::fromString('content');
$this->stream->detach();
@@ -461,11 +419,8 @@ public function isWritable_IsDetached_ReturnsFalse(): void
self::assertFalse($this->stream->isWritable());
}
- /**
- * @test
- * @dataProvider provideIsModeWriteable
- */
- public function isWritable_ReturnsExpectedValue(string $mode, bool $isWritable, string $file): void
+ #[DataProvider('provideIsModeWriteable')]
+ public function testIsWritableReturnsExpectedValue(string $mode, bool $isWritable, string $file): void
{
$file = $this->filesystem->url() . '/' . $file;
$this->stream = Stream::fromFile($file, $mode);
@@ -473,10 +428,7 @@ public function isWritable_ReturnsExpectedValue(string $mode, bool $isWritable,
self::assertSame($isWritable, $this->stream->isWritable());
}
- /**
- * @test
- */
- public function isReadable_IsDetached_ReturnsFalse(): void
+ public function testIsReadableIsDetachedReturnsFalse(): void
{
$this->stream = Stream::fromString('content');
$this->stream->detach();
@@ -484,11 +436,8 @@ public function isReadable_IsDetached_ReturnsFalse(): void
self::assertFalse($this->stream->isReadable());
}
- /**
- * @test
- * @dataProvider provideIsReadable
- */
- public function isReadable_ReturnsExpectedValue(string $mode, bool $isReadable, string $file): void
+ #[DataProvider('provideIsReadable')]
+ public function testIsReadableReturnsExpectedValue(string $mode, bool $isReadable, string $file): void
{
$file = $this->filesystem->url() . '/' . $file;
$this->stream = Stream::fromFile($file, $mode);
@@ -496,10 +445,7 @@ public function isReadable_ReturnsExpectedValue(string $mode, bool $isReadable,
self::assertSame($isReadable, $this->stream->isReadable());
}
- /**
- * @test
- */
- public function write_IsDetached_ThrowsException(): void
+ public function testWriteIsDetachedThrowsException(): void
{
$this->stream = Stream::fromFileMode('r+');
$this->stream->detach();
@@ -508,40 +454,38 @@ public function write_IsDetached_ThrowsException(): void
$this->stream->write('test');
}
- /**
- * @test
- */
- public function write_IsNotWriteable_ThrowsException(): void
+ public function testWriteIsNotWriteableThrowsException(): void
{
$this->stream = Stream::fromFileMode('r');
$this->expectException(RuntimeException::class);
- $this->expectErrorMessage('Stream is not writeable');
+ $this->expectExceptionMessage('Stream is not writeable');
$this->stream->write('test');
}
- /**
- * @test
- * @runInSeparateProcess
- */
- public function write_FWriteReturnFalse_ThrowsException(): void
+ public function testWriteFWriteReturnFalseThrowsException(): void
{
- $this->globalProphecy->fwrite(Argument::type('resource'), 'test')
- ->willReturn(false)
- ->shouldBeCalled();
+ // generate read only resource
+ $resourceMock = fopen('php://memory', 'r');
- $this->globalProphecy->reveal();
- $this->stream = Stream::fromFileMode('r+');
+ $streamHandler = $this->getMockBuilder(Stream::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getResource', 'isWritable'])
+ ->getMock();
+
+ $streamHandler->expects($this->once())
+ ->method('isWritable')
+ ->willReturn(true);
+ $streamHandler->expects($this->exactly(2))
+ ->method('getResource')
+ ->willReturn($resourceMock);
$this->expectException(RuntimeException::class);
- $this->expectErrorMessage("Cant't write to stream");
+ $this->expectExceptionMessage('Can\'t write to stream');
- $this->stream->write('test');
+ $streamHandler->write('test');
}
- /**
- * @test
- */
- public function write_ReturnNumberOfBytesWritten(): void
+ public function testWriteReturnNumberOfBytesWritten(): void
{
$expectedString = 'Some content string';
$expectedBytes = strlen($expectedString);
@@ -551,107 +495,101 @@ public function write_ReturnNumberOfBytesWritten(): void
self::assertSame($expectedString, $this->stream->__toString());
}
- /**
- * @test
- */
- public function read_IsDetached_ThrowsException(): void
+ public function testReadIsDetachedThrowsException(): void
{
$this->stream = Stream::fromFile($this->filesystem->url() . '/generated.json');
$this->stream->detach();
$this->expectException(RuntimeException::class);
- $this->expectErrorMessage('Stream is detached');
+ $this->expectExceptionMessage('Stream is detached');
$this->stream->read(100);
}
- /**
- * @test
- */
- public function read_IsNotReadable_ThrowsException(): void
+ public function testReadIsNotReadableThrowsException(): void
{
$this->stream = Stream::fromFile($this->filesystem->url() . '/generated.json', 'w');
$this->expectException(RuntimeException::class);
- $this->expectErrorMessage('Stream is not readable');
+ $this->expectExceptionMessage('Stream is not readable');
$this->stream->read(100);
}
- /**
- * @test
- * @runInSeparateProcess
- */
- public function read_FReadReturnsFalse_ThrowsException(): void
+ public function testReadFReadReturnsFalseThrowsException(): void
{
- $this->globalProphecy->fread(Argument::type('resource'), 100)
- ->willReturn(false)
- ->shouldBeCalled();
+ stream_filter_register('false_filter', FalseReturningFilter::class);
+ $resourceMock = fopen('php://memory', 'r+');
+ fwrite($resourceMock, 'Some content string');
+ stream_filter_append($resourceMock, 'false_filter');
- $this->globalProphecy->reveal();
+ $streamHandler = $this->getMockBuilder(Stream::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getResource', 'isReadable'])
+ ->getMock();
- $this->stream = Stream::fromFile($this->filesystem->url() . '/generated.json', 'r+');
+ $streamHandler->expects($this->once())
+ ->method('isReadable')
+ ->willReturn(true);
+
+ $streamHandler->expects($this->exactly(2))
+ ->method('getResource')
+ ->willReturn($resourceMock);
$this->expectException(RuntimeException::class);
- $this->expectErrorMessage("Can't read from stream");
+ $this->expectExceptionMessage('Can\'t read from stream');
- $this->stream->read(100);
+ $streamHandler->read(100);
}
- /**
- * @test
- */
- public function read_ReturnValidNumberOfBytes(): void
+ public function testReadReturnValidNumberOfBytes(): void
{
$this->stream = Stream::fromFile($this->filesystem->url() . '/generated.json', 'r');
self::assertSame(100, strlen($this->stream->read(100)));
}
- /**
- * @test
- */
- public function getContent_IsDetached_ThrowsException(): void
+ public function testGetContentIsDetachedThrowsException(): void
{
$this->stream = Stream::fromFile($this->filesystem->url() . '/generated.json');
$this->stream->detach();
$this->expectException(RuntimeException::class);
- $this->expectErrorMessage('Stream is detached');
+ $this->expectExceptionMessage('Stream is detached');
$this->stream->getContents();
}
- /**
- * @test
- */
- public function getContent_IsNotReadable_ThrowsException(): void
+ public function testGetContentIsNotReadableThrowsException(): void
{
$this->stream = Stream::fromFile($this->filesystem->url() . '/generated.json', 'w');
$this->expectException(RuntimeException::class);
- $this->expectErrorMessage('Stream is not readable');
+ $this->expectExceptionMessage('Stream is not readable');
$this->stream->getContents();
}
- /**
- * @test
- * @runInSeparateProcess
- */
- public function getContent_StreamReturnsFalse_ThrowsException(): void
+ public function testGetContentStreamReturnsFalseThrowsException(): void
{
- $this->globalProphecy->stream_get_contents(Argument::type('resource'))
- ->willReturn(false)
- ->shouldBeCalled();
+ $this->markTestIncomplete('mock');
+ stream_wrapper_register('falsestream', FalseReturningStream::class);
+ $resourceMock = fopen('falsestream://test', 'w');
- $this->globalProphecy->reveal();
+ $streamHandler = $this->getMockBuilder(Stream::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getResource', 'isReadable'])
+ ->getMock();
+
+ $streamHandler->expects($this->once())
+ ->method('isReadable')
+ ->willReturn(true);
+
+ $streamHandler->expects($this->exactly(2))
+ ->method('getResource')
+ ->willReturn($resourceMock);
- $this->stream = Stream::fromFile($this->filesystem->url() . '/generated.json', 'r+');
$this->expectException(RuntimeException::class);
- $this->expectErrorMessage("Can't read content from stream");
+ $this->expectExceptionMessage("Can't read content from stream");
- $this->stream->getContents();
+ $streamHandler->getContents();
}
- /**
- * @test
- */
- public function getMetadata_KeyIsNull_ReturnsCompleteArray(): void
+ public function testGetMetadataKeyIsNullReturnsCompleteArray(): void
{
$this->stream = Stream::fromFile($this->filesystem->url() . '/generated.json', 'r+');
$metaData = $this->stream->getMetadata();
@@ -668,10 +606,7 @@ public function getMetadata_KeyIsNull_ReturnsCompleteArray(): void
self::assertArrayHasKey('uri', $metaData);
}
- /**
- * @test
- */
- public function getMetadata_WithValidKey_ReturnsKeyValue(): void
+ public function testGetMetadataWithValidKeyReturnsKeyValue(): void
{
$this->stream = Stream::fromFile($this->filesystem->url() . '/generated.json', 'r+');
$mode = $this->stream->getMetadata('mode');
@@ -679,10 +614,7 @@ public function getMetadata_WithValidKey_ReturnsKeyValue(): void
self::assertSame('r+', $mode);
}
- /**
- * @test
- */
- public function getMetadata_WithNonExistentKey_ReturnsNull(): void
+ public function testGetMetadataWithNonExistentKeyReturnsNull(): void
{
$this->stream = Stream::fromFile($this->filesystem->url() . '/generated.json', 'r+');
@@ -690,9 +622,9 @@ public function getMetadata_WithNonExistentKey_ReturnsNull(): void
}
/**
- * All fopen modes for the status isWriteable
+ * All fopen modes for the status isWriteable.
*/
- public function provideIsModeWriteable(): array
+ public static function provideIsModeWriteable(): array
{
return [
['r', false, 'generated.json'],
@@ -709,9 +641,9 @@ public function provideIsModeWriteable(): array
}
/**
- * All fopen modes for the status isReadable
+ * All fopen modes for the status isReadable.
*/
- public function provideIsReadable(): array
+ public static function provideIsReadable(): array
{
return [
['r', true, 'generated.json'],
@@ -726,5 +658,4 @@ public function provideIsReadable(): array
['c+', true, 'generated.json'],
];
}
-
}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
deleted file mode 100644
index 83c14d9..0000000
--- a/tests/bootstrap.php
+++ /dev/null
@@ -1,19 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-error_reporting(E_ALL);
-
-ini_set('display_errors', 1);
-ini_set('zend.assertions', 0);
-
-ini_set('memory_limit', '1024m');
-
-require dirname(__DIR__) . '/vendor/autoload.php';
diff --git a/tests/phpunit.xml b/tests/phpunit.xml
deleted file mode 100644
index faeaba0..0000000
--- a/tests/phpunit.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
- ../src
-
-
-
-
- Unit
-
-
- Integration
-
-
-