diff --git a/.gitattributes b/.gitattributes index 5380433..82ad3c9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ -.gitattributes export-ignore +.github export-ignore .gitignore export-ignore .travis.yml export-ignore composer.json export-ignore diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..0c2653d --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,27 @@ +name: Tests + +on: [push, pull_request] + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: [8.0, 8.1, 8.2, 8.3, 8.4, 8.5] + + name: PHP ${{ matrix.php }} tests + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - run: composer install --no-progress --prefer-dist + - run: vendor/bin/tester tests -s + - if: failure() + uses: actions/upload-artifact@v4 + with: + name: output + path: tests/**/output diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0c4f559..0000000 --- a/.travis.yml +++ /dev/null @@ -1,24 +0,0 @@ -language: php - -php: - - 5.4 - - 5.5 - - 5.6 - - hhvm - -matrix: - allow_failures: - - php: hhvm - -before_script: - # Update composer - - composer self-update - - # Install Nette Tester - - composer install --no-interaction --dev --prefer-source - -script: vendor/bin/tester -p php -s tests - -after_failure: - # Print *.actual content - - for i in $(find tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done diff --git a/composer.json b/composer.json index 1194de8..c01c964 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "milo/github-api", "type": "library", - "description": "Github API easy access", + "description": "GitHub API easy access", "license": ["MIT"], "authors": [ { @@ -11,10 +11,10 @@ } ], "require": { - "php": ">=5.4.0" + "php": "8.0 - 8.5" }, "require-dev": { - "nette/tester": "~1.3.0" + "nette/tester": "^2.4" }, "suggest": { "ext-curl": "Allows you to use Http\\CurlClient" diff --git a/readme.md b/readme.md index f475201..00fabfc 100644 --- a/readme.md +++ b/readme.md @@ -1,23 +1,30 @@ -[Github API](https://developer.github.com/v3/) easy access library. Works with cURL, stream or any of your HTTP client. +[GitHub API](https://developer.github.com/v3/) Easy Access library +================================================================== -[![Build Status](https://travis-ci.org/milo/github-api.svg?branch=master)](https://travis-ci.org/milo/github-api) -[![Downloads last 30 days](https://img.shields.io/packagist/dm/milo/github-api.svg)](https://packagist.org/packages/milo/github-api) +Works with cURL, stream or any of your HTTP client. +[![Downloads this Month](https://img.shields.io/packagist/dm/milo/github-api.svg)](https://packagist.org/packages/milo/github-api) +[![Tests](https://github.com/milo/github-api/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/milo/github-api/actions) -# Installation + +Installation +------------ Download [release](https://github.com/milo/github-api/releases), decompress and include `github-api.php` manually, or use [Composer](https://getcomposer.org/): + ``` composer require milo/github-api ``` -# Documentation +Documentation +------------- Everything at [wiki pages](https://github.com/milo/github-api/wiki), including the [short classes description](https://github.com/milo/github-api/wiki/Classes-description). Quick Start -=========== -List all emojis used on Github. +----------- +List all emojis used on GitHub. + ```php use Milo\Github; @@ -41,7 +48,8 @@ stdClass Object ( ``` -# License +License +------- The MIT License (MIT) Copyright (c) 2014 Miloslav Hůla diff --git a/src/Github/Api.php b/src/Github/Api.php index afa6d31..94a0613 100644 --- a/src/Github/Api.php +++ b/src/Github/Api.php @@ -1,5 +1,7 @@ client = $client ?: Helpers::createDefaultClient(); } - /** - * @return self - */ - public function setToken(OAuth\Token $token = NULL) + public function setToken(?OAuth\Token $token = null): static { $this->token = $token; return $this; } - /** - * @return OAuth\Token|NULL - */ - public function getToken() + public function getToken(): ?OAuth\Token { return $this->token; } - /** - * @param array - * @return self - */ - public function setDefaultParameters(array $defaults = NULL) + public function setDefaultParameters(?array $defaults = null): static { $this->defaultParameters = $defaults ?: []; return $this; } - /** - * @return array - */ - public function getDefaultParameters() + public function getDefaultParameters(): array { return $this->defaultParameters; } + public function setDefaultAccept(string $accept): self + { + $this->defaultAccept = $accept; + return $this; + } + + + public function getDefaultAccept(): string + { + return $this->defaultAccept; + } + + /** * @see createRequest() * @see request() * - * @param string - * @return Http\Response - * * @throws MissingParameterException */ - public function delete($urlPath, array $parameters = [], array $headers = []) + public function delete(string $urlPath, array $parameters = [], array $headers = []): Http\Response { return $this->request( $this->createRequest(Http\Request::DELETE, $urlPath, $parameters, $headers) @@ -94,12 +90,9 @@ public function delete($urlPath, array $parameters = [], array $headers = []) * @see createRequest() * @see request() * - * @param string - * @return Http\Response - * * @throws MissingParameterException */ - public function get($urlPath, array $parameters = [], array $headers = []) + public function get(string $urlPath, array $parameters = [], array $headers = []): Http\Response { return $this->request( $this->createRequest(Http\Request::GET, $urlPath, $parameters, $headers) @@ -111,12 +104,9 @@ public function get($urlPath, array $parameters = [], array $headers = []) * @see createRequest() * @see request() * - * @param string - * @return Http\Response - * * @throws MissingParameterException */ - public function head($urlPath, array $parameters = [], array $headers = []) + public function head(string $urlPath, array $parameters = [], array $headers = []): Http\Response { return $this->request( $this->createRequest(Http\Request::HEAD, $urlPath, $parameters, $headers) @@ -128,14 +118,10 @@ public function head($urlPath, array $parameters = [], array $headers = []) * @see createRequest() * @see request() * - * @param string - * @param mixed - * @return Http\Response - * * @throws MissingParameterException * @throws JsonException */ - public function patch($urlPath, $content, array $parameters = [], array $headers = []) + public function patch(string $urlPath, string|array|object|null $content, array $parameters = [], array $headers = []): Http\Response { return $this->request( $this->createRequest(Http\Request::PATCH, $urlPath, $parameters, $headers, $content) @@ -147,14 +133,10 @@ public function patch($urlPath, $content, array $parameters = [], array $headers * @see createRequest() * @see request() * - * @param string - * @param mixed - * @return Http\Response - * * @throws MissingParameterException * @throws JsonException */ - public function post($urlPath, $content, array $parameters = [], array $headers = []) + public function post(string $urlPath, string|array|object|null $content, array $parameters = [], array $headers = []): Http\Response { return $this->request( $this->createRequest(Http\Request::POST, $urlPath, $parameters, $headers, $content) @@ -166,14 +148,10 @@ public function post($urlPath, $content, array $parameters = [], array $headers * @see createRequest() * @see request() * - * @param string - * @param mixed - * @return Http\Response - * * @throws MissingParameterException * @throws JsonException */ - public function put($urlPath, $content = NULL, array $parameters = [], array $headers = []) + public function put(string $urlPath, string|array|object|null $content = null, array $parameters = [], array $headers = []): Http\Response { return $this->request( $this->createRequest(Http\Request::PUT, $urlPath, $parameters, $headers, $content) @@ -182,11 +160,9 @@ public function put($urlPath, $content = NULL, array $parameters = [], array $he /** - * @return Http\Response - * * @throws Http\BadResponseException */ - public function request(Http\Request $request) + public function request(Http\Request $request): Http\Response { $request = clone $request; @@ -204,31 +180,38 @@ public function request(Http\Request $request) /** - * @param string Http\Request::GET|POST|... - * @param string path like '/users/:user/repos' where ':user' is substitution - * @param array[name => value] replaces substitutions in $urlPath, the rest is appended as query string to URL - * @param array[name => value] name is case-insensitive - * @param mixed|NULL arrays and objects are encoded to JSON and Content-Type is set - * @return Http\Request + * @param string $method Http\Request::GET|POST|... + * @param string $urlPath like '/users/:user/repos' where ':user' is substitution + * @param string[] $parameters replaces substitutions in $urlPath, the rest is appended as query string to URL + * @param string[] $headers key as case-insensitive header name + * @param string|array|object|null $content arrays and objects are encoded to JSON and Content-Type is set * * @throws MissingParameterException when substitution is used in URL but parameter is missing * @throws JsonException when encoding to JSON fails */ - public function createRequest($method, $urlPath, array $parameters = [], array $headers = [], $content = NULL) + public function createRequest(string $method, string $urlPath, array $parameters = [], array $headers = [], string|array|object|null $content = null): Http\Request { - if (stripos($urlPath, $this->url) === 0) { + if (stripos($urlPath, $this->url) === 0) { # Allows non-HTTPS URLs + $baseUrl = $this->url; $urlPath = substr($urlPath, strlen($this->url)); + + } elseif (preg_match('#^(https://[^/]+)(/.*)?$#', $urlPath, $m)) { + $baseUrl = $m[1]; + $urlPath = $m[2] ?? ''; + + } else { + $baseUrl = $this->url; } - if (strpos($urlPath, '{') === FALSE) { + if (!str_contains($urlPath, '{')) { $urlPath = $this->expandColonParameters($urlPath, $parameters, $this->defaultParameters); } else { $urlPath = $this->expandUriTemplate($urlPath, $parameters, $this->defaultParameters); } - $url = rtrim($this->url, '/') . '/' . ltrim($urlPath, '/'); + $url = rtrim($baseUrl, '/') . '/' . ltrim($urlPath, '/'); - if ($content !== NULL && (is_array($content) || is_object($content))) { + if (is_array($content) || is_object($content)) { $headers['Content-Type'] = 'application/json; charset=utf-8'; $content = Helpers::jsonEncode($content); } @@ -238,13 +221,11 @@ public function createRequest($method, $urlPath, array $parameters = [], array $ /** - * @param Http\Response - * @param array|NULL these codes are treated as success; code < 300 if NULL - * @return mixed + * @param ?int[] $okCodes these codes are treated as success; code < 300 if null * * @throws ApiException */ - public function decode(Http\Response $response, array $okCodes = NULL) + public function decode(Http\Response $response, ?array $okCodes = null): mixed { $content = $response->getContent(); if (preg_match('~application/json~i', $response->getHeader('Content-Type', ''))) { @@ -255,35 +236,35 @@ public function decode(Http\Response $response, array $okCodes = NULL) } if (!is_array($content) && !is_object($content)) { - throw new InvalidResponseException('Decoded JSON is not an array or object.', 0, NULL, $response); + throw new InvalidResponseException('Decoded JSON is not an array or object.', 0, null, $response); } } $code = $response->getCode(); - if (($okCodes === NULL && $code >= 300) || (is_array($okCodes) && !in_array($code, $okCodes))) { + if (($okCodes === null && $code >= 300) || (is_array($okCodes) && !in_array($code, $okCodes))) { /** @var $content \stdClass */ switch ($code) { case Http\Response::S400_BAD_REQUEST: - throw new BadRequestException(self::errorMessage($content), $code, NULL, $response); + throw new BadRequestException(self::errorMessage($content), $code, null, $response); case Http\Response::S401_UNAUTHORIZED: - throw new UnauthorizedException(self::errorMessage($content), $code, NULL, $response); + throw new UnauthorizedException(self::errorMessage($content), $code, null, $response); case Http\Response::S403_FORBIDDEN: if ($response->getHeader('X-RateLimit-Remaining') === '0') { - throw new RateLimitExceedException(self::errorMessage($content), $code, NULL, $response); + throw new RateLimitExceedException(self::errorMessage($content), $code, null, $response); } - throw new ForbiddenException(self::errorMessage($content), $code, NULL, $response); + throw new ForbiddenException(self::errorMessage($content), $code, null, $response); case Http\Response::S404_NOT_FOUND: - throw new NotFoundException('Resource not found or not authorized to access.', $code, NULL, $response); + throw new NotFoundException('Resource not found or not authorized to access.', $code, null, $response); case Http\Response::S422_UNPROCESSABLE_ENTITY: - throw new UnprocessableEntityException(self::errorMessage($content), $code, NULL, $response); + throw new UnprocessableEntityException(self::errorMessage($content), $code, null, $response); } - $message = $okCodes === NULL ? '< 300' : implode(' or ', $okCodes); - throw new UnexpectedResponseException("Expected response with code $message.", $code, NULL, $response); + $message = $okCodes === null ? '< 300' : implode(' or ', $okCodes); + throw new UnexpectedResponseException("Expected response with code $message.", $code, null, $response); } return $content; @@ -295,12 +276,9 @@ public function decode(Http\Response $response, array $okCodes = NULL) * * @see get() * - * @param string - * @return Paginator - * * @throws MissingParameterException */ - public function paginator($urlPath, array $parameters = [], array $headers = []) + public function paginator(string $urlPath, array $parameters = [], array $headers = []): Paginator { return new Paginator( $this, @@ -309,46 +287,41 @@ public function paginator($urlPath, array $parameters = [], array $headers = []) } - /** - * @return Http\IClient - */ - public function getClient() + public function getClient(): Http\IClient { return $this->client; } - /** - * @param string - * @return self - */ - public function setUrl($url) + public function withUrl(string $url): Api + { + $api = clone $this; + $api->setUrl($url); + return $api; + } + + + public function setUrl(string $url): static { $this->url = $url; return $this; } - /** - * @return string - */ - public function getUrl() + public function getUrl(): string { return $this->url; } /** - * @param string - * @return string - * * @throws MissingParameterException */ - protected function expandColonParameters($url, array $parameters, array $defaultParameters) + protected function expandColonParameters(string $url, array $parameters, array $defaultParameters): string { $parameters += $defaultParameters; - $url = preg_replace_callback('#(^|/):([^/]+)#', function($m) use ($url, & $parameters) { + $url = preg_replace_callback('#(^|/|\.):([^/.]+)#', function($m) use ($url, &$parameters) { if (!isset($parameters[$m[2]])) { throw new MissingParameterException("Missing parameter '$m[2]' for URL path '$url'."); } @@ -372,38 +345,35 @@ protected function expandColonParameters($url, array $parameters, array $default * * @see http://tools.ietf.org/html/rfc6570 * @todo Inject remaining default parameters into query string? - * - * @param string - * @return string */ - protected function expandUriTemplate($url, array $parameters, array $defaultParameters) + protected function expandUriTemplate(string $url, array $parameters, array $defaultParameters): string { $parameters += $defaultParameters; static $operatorFlags = [ - '' => ['prefix' => '', 'separator' => ',', 'named' => FALSE, 'ifEmpty' => '', 'reserved' => FALSE], - '+' => ['prefix' => '', 'separator' => ',', 'named' => FALSE, 'ifEmpty' => '', 'reserved' => TRUE], - '#' => ['prefix' => '#', 'separator' => ',', 'named' => FALSE, 'ifEmpty' => '', 'reserved' => TRUE], - '.' => ['prefix' => '.', 'separator' => '.', 'named' => FALSE, 'ifEmpty' => '', 'reserved' => FALSE], - '/' => ['prefix' => '/', 'separator' => '/', 'named' => FALSE, 'ifEmpty' => '', 'reserved' => FALSE], - ';' => ['prefix' => ';', 'separator' => ';', 'named' => TRUE, 'ifEmpty' => '', 'reserved' => FALSE], - '?' => ['prefix' => '?', 'separator' => '&', 'named' => TRUE, 'ifEmpty' => '=', 'reserved' => FALSE], - '&' => ['prefix' => '&', 'separator' => '&', 'named' => TRUE, 'ifEmpty' => '=', 'reserved' => FALSE], + '' => ['prefix' => '', 'separator' => ',', 'named' => false, 'ifEmpty' => '', 'reserved' => false], + '+' => ['prefix' => '', 'separator' => ',', 'named' => false, 'ifEmpty' => '', 'reserved' => true], + '#' => ['prefix' => '#', 'separator' => ',', 'named' => false, 'ifEmpty' => '', 'reserved' => true], + '.' => ['prefix' => '.', 'separator' => '.', 'named' => false, 'ifEmpty' => '', 'reserved' => false], + '/' => ['prefix' => '/', 'separator' => '/', 'named' => false, 'ifEmpty' => '', 'reserved' => false], + ';' => ['prefix' => ';', 'separator' => ';', 'named' => true, 'ifEmpty' => '', 'reserved' => false], + '?' => ['prefix' => '?', 'separator' => '&', 'named' => true, 'ifEmpty' => '=', 'reserved' => false], + '&' => ['prefix' => '&', 'separator' => '&', 'named' => true, 'ifEmpty' => '=', 'reserved' => false], ]; - return preg_replace_callback('~{([+#./;?&])?([^}]+?)}~', function($m) use ($url, & $parameters, $operatorFlags) { + return preg_replace_callback('~{([+#./;?&])?([^}]+?)}~', function($m) use ($url, &$parameters, $operatorFlags) { $flags = $operatorFlags[$m[1]]; $translated = []; foreach (explode(',', $m[2]) as $name) { - $explode = FALSE; - $maxLength = NULL; + $explode = false; + $maxLength = null; if (preg_match('~^(.+)(?:(\*)|:(\d+))$~', $name, $tmp)) { // TODO: Speed up? $name = $tmp[1]; if (isset($tmp[3])) { $maxLength = (int) $tmp[3]; } else { - $explode = TRUE; + $explode = true; } } @@ -420,20 +390,20 @@ protected function expandUriTemplate($url, array $parameters, array $defaultPara $isAssoc = key($value) !== 0; // The '*' (explode) modifier + $parts = []; if ($explode) { - $parts = []; if ($isAssoc) { - $this->walk($value, function ($v, $k) use (& $parts, $flags, $maxLength) { - $parts[] = $this->prefix(['named' => TRUE] + $flags, $k, $this->escape($flags, $v, $maxLength)); + $this->walk($value, function ($v, $k) use (&$parts, $flags, $maxLength) { + $parts[] = $this->prefix(['named' => true] + $flags, $k, $this->escape($flags, $v, $maxLength)); }); } elseif ($flags['named']) { - $this->walk($value, function ($v) use (& $parts, $flags, $name, $maxLength) { + $this->walk($value, function ($v) use (&$parts, $flags, $name, $maxLength) { $parts[] = $this->prefix($flags, $name, $this->escape($flags, $v, $maxLength)); }); } else { - $this->walk($value, function ($v) use (& $parts, $flags, $maxLength) { + $this->walk($value, function ($v) use (&$parts, $flags, $maxLength) { $parts[] = $this->escape($flags, $v, $maxLength); }); } @@ -447,8 +417,7 @@ protected function expandUriTemplate($url, array $parameters, array $defaultPara } } else { - $parts = []; - $this->walk($value, function($v, $k) use (& $parts, $isAssoc, $flags, $maxLength) { + $this->walk($value, function($v, $k) use (&$parts, $isAssoc, $flags, $maxLength) { if ($isAssoc) { $parts[] = $this->escape($flags, $k); } @@ -473,12 +442,9 @@ protected function expandUriTemplate($url, array $parameters, array $defaultPara /** - * @param array - * @param string - * @param string already escaped - * @return string + * @param string $value already escaped */ - private function prefix(array $flags, $name, $value) + private function prefix(array $flags, string $name, string $value): string { $prefix = ''; if ($flags['named']) { @@ -494,17 +460,11 @@ private function prefix(array $flags, $name, $value) } - /** - * @param array - * @param mixed - * @param int|NULL - * @return string - */ - private function escape(array $flags, $value, $maxLength = NULL) + private function escape(array $flags, string|int|false $value, ?int $maxLength = null): string { $value = (string) $value; - if ($maxLength !== NULL) { + if ($maxLength !== null) { if (preg_match('~^(.{' . $maxLength . '}).~u', $value, $m)) { $value = $m[1]; } elseif (strlen($value) > $maxLength) { # when malformed UTF-8 @@ -528,14 +488,10 @@ private function escape(array $flags, $value, $maxLength = NULL) } - /** - * @param array - * @param callable - */ - private function walk(array $array, $cb) + private function walk(array $array, callable $cb): void { foreach ($array as $k => $v) { - if ($v === NULL) { + if ($v === null) { continue; } @@ -544,15 +500,9 @@ private function walk(array $array, $cb) } - /** - * @param \stdClass - * @return string - */ - private static function errorMessage($content) + private static function errorMessage(\stdClass $content): string { - $message = isset($content->message) - ? $content->message - : 'Unknown error'; + $message = $content->message ?? 'Unknown error'; if (isset($content->errors)) { $message .= implode(', ', array_map(function($error) { @@ -562,5 +512,4 @@ private static function errorMessage($content) return $message; } - } diff --git a/src/Github/Helpers.php b/src/Github/Helpers.php index 1346dc3..9b22d7a 100644 --- a/src/Github/Helpers.php +++ b/src/Github/Helpers.php @@ -1,5 +1,7 @@ 'The maximum stack depth has been exceeded', - JSON_ERROR_STATE_MISMATCH => 'Syntax error, malformed JSON', - JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', - JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', - JSON_ERROR_UTF8 => 'Invalid UTF-8 sequence', - ]; - - - /** @var Http\IClient */ - private static $client; + private static Http\IClient $client; /** - * @param mixed - * @return string - * * @throws JsonException */ - public static function jsonEncode($value) + public static function jsonEncode(mixed $value): string { - if (PHP_VERSION_ID < 50500) { - set_error_handler(function($severity, $message) { // needed to receive 'recursion detected' error - restore_error_handler(); - throw new JsonException($message); - }); - } - $json = json_encode($value, JSON_UNESCAPED_UNICODE); - - if (PHP_VERSION_ID < 50500) { - restore_error_handler(); - } - if ($error = json_last_error()) { - $message = isset(static::$jsonMessages[$error]) - ? static::$jsonMessages[$error] - : (PHP_VERSION_ID >= 50500 ? json_last_error_msg() : 'Unknown error'); - - throw new JsonException($message, $error); + throw new JsonException(json_last_error_msg(), $error); } - - $json = str_replace(array("\xe2\x80\xa8", "\xe2\x80\xa9"), array('\u2028', '\u2029'), $json); return $json; } /** - * @param mixed - * @return string - * * @throws JsonException */ - public static function jsonDecode($json) + public static function jsonDecode(string $json): mixed { - $json = (string) $json; - if (!preg_match('##u', $json)) { - throw new JsonException('Invalid UTF-8 sequence', 5); // PECL JSON-C - } - - $value = json_decode($json, FALSE, 512, (defined('JSON_C_VERSION') && PHP_INT_SIZE > 4) ? 0 : JSON_BIGINT_AS_STRING); - - if ($value === NULL && $json !== '' && strcasecmp($json, 'null')) { // '' does not clear json_last_error() - $error = json_last_error(); - throw new JsonException(isset(static::$jsonMessages[$error]) ? static::$jsonMessages[$error] : 'Unknown error', $error); + $value = json_decode($json, false, 512, JSON_BIGINT_AS_STRING); + if ($error = json_last_error()) { + throw new JsonException(json_last_error_msg(), $error); } return $value; } - /** - * @param bool - * @return Http\IClient - */ - public static function createDefaultClient($newInstance = FALSE) + public static function createDefaultClient(bool $newInstance = false): Http\IClient { - if (self::$client === NULL || $newInstance) { + if (empty(self::$client) || $newInstance) { self::$client = extension_loaded('curl') ? new Http\CurlClient : new Http\StreamClient; @@ -97,5 +54,4 @@ public static function createDefaultClient($newInstance = FALSE) return self::$client; } - } diff --git a/src/Github/Http/AbstractClient.php b/src/Github/Http/AbstractClient.php index 5cd036d..fd1644d 100644 --- a/src/Github/Http/AbstractClient.php +++ b/src/Github/Http/AbstractClient.php @@ -1,5 +1,7 @@ maxRedirects; - $previous = NULL; + $previous = null; do { $this->setupRequest($request); @@ -65,45 +67,36 @@ public function request(Request $request) } break; - } while (TRUE); + } while (true); return $response; } - /** - * @param callable|NULL function(Request $request) - * @return self - */ - public function onRequest($callback) + /** @inheritdoc */ + public function onRequest(?callable $callback): static { $this->onRequest = $callback; return $this; } - /** - * @param callable|NULL function(Response $response) - * @return self - */ - public function onResponse($callback) + /** @inheritdoc */ + public function onResponse(?callable $callback): static { $this->onResponse = $callback; return $this; } - protected function setupRequest(Request $request) + protected function setupRequest(Request $request): void { $request->addHeader('Expect', ''); } /** - * @return Response - * * @throws BadResponseException */ - abstract protected function process(Request $request); - + abstract protected function process(Request $request): Response; } diff --git a/src/Github/Http/CachedClient.php b/src/Github/Http/CachedClient.php index f7c5f85..967df83 100644 --- a/src/Github/Http/CachedClient.php +++ b/src/Github/Http/CachedClient.php @@ -1,5 +1,7 @@ cache = $cache; + public function __construct( + private Storages\ICache $cache, + ?IClient $client = null, + private bool $forbidRecheck = false, + ) { $this->client = $client ?: Github\Helpers::createDefaultClient(); - $this->forbidRecheck = (bool) $forbidRecheck; } - /** - * @return IClient - */ - public function getInnerClient() + public function getInnerClient(): IClient { return $this->client; } /** - * @return Response - * * @throws BadResponseException */ - public function request(Request $request) + public function request(Request $request): Response { $request = clone $request; @@ -77,7 +68,8 @@ public function request(Request $request) /** @var $cached Response */ if ($cached->hasHeader('Last-Modified')) { $request->addHeader('If-Modified-Since', $cached->getHeader('Last-Modified')); - } elseif ($cached->hasHeader('ETag')) { + } + if ($cached->hasHeader('ETag')) { $request->addHeader('If-None-Match', $cached->getHeader('ETag')); } } @@ -101,42 +93,33 @@ public function request(Request $request) } - /** - * @param callable|NULL function(Request $request) - * @return self - */ - public function onRequest($callback) + /** @inheritdoc */ + public function onRequest(?callable $callback): static { $this->client->onRequest($callback); return $this; } - /** - * @param callable|NULL function(Response $response) - * @return self - */ - public function onResponse($callback) + /** @inheritdoc */ + public function onResponse(?callable $callback): static { - $this->client->onResponse(NULL); + $this->client->onResponse(null); $this->onResponse = $callback; return $this; } - /** - * @return bool - */ - protected function isCacheable(Response $response) + protected function isCacheable(Response $response): bool { /** @todo Do it properly. Vary:, Pragma:, TTL... */ if (!$response->isCode(200)) { - return FALSE; + return false; + } elseif (preg_match('#max-age=0|must-revalidate#i', $response->getHeader('Cache-Control', ''))) { - return FALSE; + return false; } return $response->hasHeader('ETag') || $response->hasHeader('Last-Modified'); } - } diff --git a/src/Github/Http/CurlClient.php b/src/Github/Http/CurlClient.php index 06d3d06..0b072c3 100644 --- a/src/Github/Http/CurlClient.php +++ b/src/Github/Http/CurlClient.php @@ -1,5 +1,7 @@ options = $options; } - protected function setupRequest(Request $request) + protected function setupRequest(Request $request): void { parent::setupRequest($request); $request->addHeader('Connection', 'keep-alive'); @@ -42,11 +39,9 @@ protected function setupRequest(Request $request) /** - * @return Response - * * @throws BadResponseException */ - protected function process(Request $request) + protected function process(Request $request): Response { $headers = []; foreach ($request->getHeaders() as $name => $value) { @@ -63,26 +58,26 @@ protected function process(Request $request) ]; $hardOptions = [ - CURLOPT_FOLLOWLOCATION => FALSE, # Github sets the Location header for 201 code too and redirection is not required for us + CURLOPT_FOLLOWLOCATION => false, # Github sets the Location header for 201 code too and redirection is not required for us CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => $request->getMethod(), CURLOPT_NOBODY => $request->isMethod(Request::HEAD), CURLOPT_URL => $request->getUrl(), CURLOPT_HTTPHEADER => $headers, - CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_RETURNTRANSFER => true, CURLOPT_POSTFIELDS => $request->getContent(), - CURLOPT_HEADER => FALSE, - CURLOPT_HEADERFUNCTION => function($curl, $line) use (& $responseHeaders, & $last) { + CURLOPT_HEADER => false, + CURLOPT_HEADERFUNCTION => function($curl, $line) use (&$responseHeaders, &$last) { if (strncasecmp($line, 'HTTP/', 5) === 0) { /** @todo Set proxy response as Response::setPrevious($proxyResponse)? */ # The HTTP/x.y may occur multiple times with proxy (HTTP/1.1 200 Connection Established) $responseHeaders = []; - } elseif (in_array(substr($line, 0, 1), [' ', "\t"], TRUE)) { + } elseif (in_array(substr($line, 0, 1), [' ', "\t"], true)) { $responseHeaders[$last] .= ' ' . trim($line); # RFC2616, 2.2 } elseif ($line !== "\r\n") { - list($name, $value) = explode(':', $line, 2); + [$name, $value] = explode(':', $line, 2); $responseHeaders[$last = trim($name)] = trim($value); } @@ -96,27 +91,26 @@ protected function process(Request $request) if (!$this->curl) { $this->curl = curl_init(); - if ($this->curl === FALSE) { + if ($this->curl === false) { throw new BadResponseException('Cannot init cURL handler.'); } } $result = curl_setopt_array($this->curl, $hardOptions + ($this->options ?: []) + $softOptions); - if ($result === FALSE) { + if ($result === false) { throw new BadResponseException('Setting cURL options failed: ' . curl_error($this->curl), curl_errno($this->curl)); } $content = curl_exec($this->curl); - if ($content === FALSE) { + if ($content === false) { throw new BadResponseException(curl_error($this->curl), curl_errno($this->curl)); } $code = curl_getinfo($this->curl, CURLINFO_HTTP_CODE); - if ($code === FALSE) { + if ($code === false) { throw new BadResponseException('HTTP status code is missing:' . curl_error($this->curl), curl_errno($this->curl)); } - return new Response($code, $responseHeaders, $content); + return new Response((int) $code, $responseHeaders, $content); } - } diff --git a/src/Github/Http/IClient.php b/src/Github/Http/IClient.php index 966cd75..4d61917 100644 --- a/src/Github/Http/IClient.php +++ b/src/Github/Http/IClient.php @@ -1,5 +1,7 @@ value] */ - private $headers = []; + use Github\Strict; - /** @var string|NULL */ - private $content; + private array $headers = []; - /** - * @param array - * @param string|NULL - */ - public function __construct(array $headers = [], $content = NULL) - { - $this->headers = array_change_key_case($headers, CASE_LOWER); - $this->content = $content; + public function __construct( + array $headers = [], + private ?string $content = null, + ) { + foreach ($headers as $name => $value) { + $this->setHeader($name, $value); + } } - /** - * @param string - * @return bool - */ - public function hasHeader($name) + public function hasHeader(string $name): bool { return array_key_exists(strtolower($name), $this->headers); } - /** - * @param string - * @param mixed - * @return mixed - */ - public function getHeader($name, $default = NULL) + public function getHeader(string $name, ?string $default = null): ?string { $name = strtolower($name); return array_key_exists($name, $this->headers) @@ -54,15 +44,10 @@ public function getHeader($name, $default = NULL) } - /** - * @param string - * @param string - * @return self - */ - protected function addHeader($name, $value) + protected function addHeader(string $name, ?string $value): static { $name = strtolower($name); - if (!array_key_exists($name, $this->headers) && $value !== NULL) { + if (!array_key_exists($name, $this->headers) && $value !== null) { $this->headers[$name] = $value; } @@ -70,15 +55,10 @@ protected function addHeader($name, $value) } - /** - * @param string - * @param string|NULL - * @return self - */ - protected function setHeader($name, $value) + protected function setHeader(string $name, ?string $value): static { $name = strtolower($name); - if ($value === NULL) { + if ($value === null) { unset($this->headers[$name]); } else { $this->headers[$name] = $value; @@ -89,20 +69,16 @@ protected function setHeader($name, $value) /** - * @return array + * @return string[] */ - public function getHeaders() + public function getHeaders(): array { return $this->headers; } - /** - * @return string|NULL - */ - public function getContent() + public function getContent(): ?string { return $this->content; } - } diff --git a/src/Github/Http/Request.php b/src/Github/Http/Request.php index 9391c61..4ac0207 100644 --- a/src/Github/Http/Request.php +++ b/src/Github/Http/Request.php @@ -1,8 +1,8 @@ method = $method; - $this->url = $url; + public function __construct( + private string $method, + private string $url, + array $headers = [], + ?string $content = null + ) { parent::__construct($headers, $content); } - /** - * @param string - * @return bool - */ - public function isMethod($method) + public function isMethod(string $method): bool { return strcasecmp($this->method, $method) === 0; } - /** - * @return string - */ - public function getMethod() + public function getMethod(): string { return $this->method; } - /** - * @return string - */ - public function getUrl() + public function getUrl(): string { return $this->url; } - /** - * @param string - * @param string - * @return self - */ - public function addHeader($name, $value) + public function addHeader(string $name, ?string $value): static { return parent::addHeader($name, $value); } - /** - * @param string - * @param string|NULL - * @return self - */ - public function setHeader($name, $value) + public function setHeader(string $name, ?string $value): static { return parent::setHeader($name, $value); } - } diff --git a/src/Github/Http/Response.php b/src/Github/Http/Response.php index 8d20c26..be602a4 100644 --- a/src/Github/Http/Response.php +++ b/src/Github/Http/Response.php @@ -1,5 +1,7 @@ code = (int) $code; + public function __construct( + private int $code, + array $headers, + ?string $content, + ) { parent::__construct($headers, $content); } /** - * HTTP code. - * @return int + * HTTP response status code. */ - public function getCode() + public function getCode(): int { return $this->code; } - /** - * @param int - * @return bool - */ - public function isCode($code) + public function isCode(int $code): bool { - return $this->code === (int) $code; + return $this->code === $code; } - /** - * @return Response|NULL - */ - public function getPrevious() + public function getPrevious(): ?Response { return $this->previous; } /** - * @return self - * * @throws Github\LogicException */ - public function setPrevious(Response $previous = NULL) + public function setPrevious(?Response $previous = null): static { if ($this->previous) { throw new Github\LogicException('Previous response is already set.'); @@ -87,5 +72,4 @@ public function setPrevious(Response $previous = NULL) return $this; } - } diff --git a/src/Github/Http/StreamClient.php b/src/Github/Http/StreamClient.php index dbf3b73..f345d17 100644 --- a/src/Github/Http/StreamClient.php +++ b/src/Github/Http/StreamClient.php @@ -1,5 +1,7 @@ sslOptions = $sslOptions; - } + public function __construct( + private ?array $sslOptions = null, + ) {} - protected function setupRequest(Request $request) + protected function setupRequest(Request $request): void { parent::setupRequest($request); $request->setHeader('Connection', 'close'); @@ -31,11 +28,9 @@ protected function setupRequest(Request $request) /** - * @return Response - * * @throws BadResponseException */ - protected function process(Request $request) + protected function process(Request $request): Response { $headerStr = []; foreach ($request->getHeaders() as $name => $value) { @@ -50,16 +45,16 @@ protected function process(Request $request) 'header' => implode("\r\n", $headerStr) . "\r\n", 'follow_location' => 0, # Github sets the Location header for 201 code too and redirection is not required for us 'protocol_version' => 1.1, - 'ignore_errors' => TRUE, + 'ignore_errors' => true, ], 'ssl' => [ - 'verify_peer' => TRUE, + 'verify_peer' => true, 'cafile' => realpath(__DIR__ . '/../../ca-chain.crt'), - 'disable_compression' => TRUE, # Effective since PHP 5.4.13 + 'disable_compression' => true, # Effective since PHP 5.4.13 ], ]; - if (($content = $request->getContent()) !== NULL) { + if (($content = $request->getContent()) !== null) { $options['http']['content'] = $content; } @@ -67,31 +62,30 @@ protected function process(Request $request) $options['ssl'] = $this->sslOptions + $options['ssl']; } - list($code, $headers, $content) = $this->fileGetContents($request->getUrl(), $options); + [$code, $headers, $content] = $this->fileGetContents($request->getUrl(), $options); return new Response($code, $headers, $content); } /** * @internal - * @param string - * @param array - * @return array - * * @throws BadResponseException */ - protected function fileGetContents($url, array $contextOptions) + protected function fileGetContents(string $url, array $contextOptions): array { $context = stream_context_create($contextOptions); - $e = NULL; - set_error_handler(function($severity, $message, $file, $line) use (& $e) { + $e = null; + set_error_handler(function($severity, $message, $file, $line) use (&$e) { $e = new \ErrorException($message, 0, $severity, $file, $line, $e); }, E_WARNING); - $content = file_get_contents($url, FALSE, $context); + $content = file_get_contents($url, false, $context); restore_error_handler(); + if (PHP_VERSION_ID >= 80400) { + $http_response_header = http_get_last_response_headers(); + } if (!isset($http_response_header)) { throw new BadResponseException('Missing HTTP headers, request failed.', 0, $e); } @@ -101,17 +95,17 @@ protected function fileGetContents($url, array $contextOptions) } unset($http_response_header[0]); + $last = null; $headers = []; foreach ($http_response_header as $header) { - if (in_array(substr($header, 0, 1), [' ', "\t"], TRUE)) { + if (in_array(substr($header, 0, 1), [' ', "\t"], true)) { $headers[$last] .= ' ' . trim($header); # RFC2616, 2.2 } else { - list($name, $value) = explode(':', $header, 2) + [NULL, NULL]; + [$name, $value] = explode(':', $header, 2) + [null, null]; $headers[$last = trim($name)] = trim($value); } } - return [$m[1], $headers, $content]; + return [(int) $m[1], $headers, $content]; } - } diff --git a/src/Github/OAuth/Configuration.php b/src/Github/OAuth/Configuration.php index e5ae7cd..2bd8c59 100644 --- a/src/Github/OAuth/Configuration.php +++ b/src/Github/OAuth/Configuration.php @@ -1,5 +1,7 @@ clientId = $clientId; - $this->clientSecret = $clientSecret; - $this->scopes = $scopes; - } + public function __construct( + public string $clientId, + public string $clientSecret, + public array $scopes = [], + ) {} - /** - * @return Configuration - */ - public static function fromArray(array $conf) + public static function fromArray(array $conf): static { - return new static($conf['clientId'], $conf['clientSecret'], isset($conf['scopes']) ? $conf['scopes'] : []); + return new static( + $conf['clientId'], + $conf['clientSecret'], + $conf['scopes'] ?? [], + ); } - } diff --git a/src/Github/OAuth/Login.php b/src/Github/OAuth/Login.php index 4e0fac1..7a8b9da 100644 --- a/src/Github/OAuth/Login.php +++ b/src/Github/OAuth/Login.php @@ -1,10 +1,12 @@ conf = $conf; + public function __construct( + private Configuration $conf, + ?Storages\ISessionStorage $storage = null, + ?Http\IClient $client = null + ) { $this->storage = $storage ?: new Storages\SessionStorage; $this->client = $client ?: Github\Helpers::createDefaultClient(); } - /** - * @return Http\IClient - */ - public function getClient() + public function getClient(): Http\IClient { return $this->client; } /** - * @param string URL to redirect back from Github when user approves the permissions request - * @param callable function($githubUrl) makes HTTP redirect to Github + * @param string $backUrl URL to redirect back from GitHub when user approves the permissions request + * @param ?callable $redirectCb makes HTTP redirect to GitHub */ - public function askPermissions($backUrl, $redirectCb = NULL) + public function askPermissions(string $backUrl, ?callable $redirectCb = null): void { /** @todo Something more safe? */ - $state = sha1(uniqid(microtime(TRUE), TRUE)); + $state = sha1(uniqid((string) microtime(true), true)); $params = [ 'client_id' => $this->conf->clientId, 'redirect_uri' => $backUrl, @@ -65,7 +61,7 @@ public function askPermissions($backUrl, $redirectCb = NULL) $this->storage->set('auth.state', $state); $url = $this->authUrl . '?' . http_build_query($params); - if ($redirectCb === NULL) { + if ($redirectCb === null) { header("Location: $url"); die(); } else { @@ -75,13 +71,9 @@ public function askPermissions($backUrl, $redirectCb = NULL) /** - * @param string - * @param string - * @return Token - * * @throws LoginException */ - public function obtainToken($code, $state) + public function obtainToken(string $code, string $state): Token { if ($state !== $this->storage->get('auth.state')) { throw new LoginException('OAuth security state does not match.'); @@ -122,45 +114,41 @@ public function obtainToken($code, $state) } $token = new Token($json->access_token, $json->token_type, strlen($json->scope) ? explode(',', $json->scope) : []); - $this->storage->set('auth.token', $token); + $this->storage->set('auth.token', $token->toArray()); $this->storage->remove('auth.state'); return $token; } - /** - * @return bool - */ - public function hasToken() + public function hasToken(): bool { - return $this->storage->get('auth.token') !== NULL; + return $this->storage->get('auth.token') !== null; } /** - * @return Token - * * @throws Github\LogicException when token has not been obtained yet */ - public function getToken() + public function getToken(): Token { $token = $this->storage->get('auth.token'); - if ($token === NULL) { + if ($token === null) { throw new Github\LogicException('Token has not been obtained yet.'); + + } elseif ($token instanceof Token) { + /** @deprecated */ + $token = $token->toArray(); + $this->storage->set('auth.token', $token); } - return $token; + return Token::createFromArray($token); } - /** - * @return self - */ - public function dropToken() + public function dropToken(): static { $this->storage->remove('auth.token'); return $this; } - } diff --git a/src/Github/OAuth/Token.php b/src/Github/OAuth/Token.php index e95a3e6..c1ad60e 100644 --- a/src/Github/OAuth/Token.php +++ b/src/Github/OAuth/Token.php @@ -1,5 +1,7 @@ value = $value; - $this->type = $type; - $this->scopes = $scopes; - } + public function __construct( + private string $value, + private string $type = '', + private array $scopes = [] + ) {} - /** - * @return string - */ - public function getValue() + public function getValue(): string { return $this->value; } - /** - * @return string - */ - public function getType() + public function getType(): string { return $this->type; } @@ -56,7 +42,7 @@ public function getType() /** * @return string[] */ - public function getScopes() + public function getScopes(): array { return $this->scopes; } @@ -64,14 +50,11 @@ public function getScopes() /** * @see https://developer.github.com/v3/oauth/#scopes - * - * @param string - * @return bool */ - public function hasScope($scope) + public function hasScope(string $scope): bool { - if (in_array($scope, $this->scopes, TRUE)) { - return TRUE; + if (in_array($scope, $this->scopes, true)) { + return true; } static $superiors = [ @@ -80,11 +63,28 @@ public function hasScope($scope) 'notifications' => 'repo', ]; - if (array_key_exists($scope, $superiors) && in_array($superiors[$scope], $this->scopes, TRUE)) { - return TRUE; + if (array_key_exists($scope, $superiors) && in_array($superiors[$scope], $this->scopes, true)) { + return true; } - return FALSE; + return false; } + + /** @internal */ + public function toArray(): array + { + return [ + 'value' => $this->value, + 'type' => $this->type, + 'scopes' => $this->scopes, + ]; + } + + + /** @internal */ + public static function createFromArray(array $data): static + { + return new static($data['value'], $data['type'], $data['scopes']); + } } diff --git a/src/Github/Paginator.php b/src/Github/Paginator.php index 8829d5f..70f461d 100644 --- a/src/Github/Paginator.php +++ b/src/Github/Paginator.php @@ -1,106 +1,82 @@ api = $api; + public function __construct( + private Api $api, + Http\Request $request + ) { $this->firstRequest = clone $request; } /** * Limits maximum steps of iteration. - * - * @param int|NULL - * @return self */ - public function limit($limit) + public function limit(?int $limit): static { - $this->limit = $limit === NULL - ? NULL - : (int) $limit; - + $this->limit = $limit; return $this; } - /** - * @return void - */ - public function rewind() + public function rewind(): void { $this->request = $this->firstRequest; - $this->response = NULL; + $this->response = null; $this->counter = 0; } - /** - * @return bool - */ - public function valid() + public function valid(): bool { - return $this->request !== NULL && ($this->limit === NULL || $this->counter < $this->limit); + return $this->request !== null && ($this->limit === null || $this->counter < $this->limit); } - /** - * @return Http\Response - */ - public function current() + public function current(): Http\Response { $this->load(); return $this->response; } - /** - * @return int - */ - public function key() + public function key(): int { return static::parsePage($this->request->getUrl()); } - /** - * @return void - */ - public function next() + public function next(): void { $this->load(); - if ($url = static::parseLink($this->response->getHeader('Link'), 'next')) { + if ($url = static::parseLink((string) $this->response->getHeader('Link'), 'next')) { $this->request = new Http\Request( $this->request->getMethod(), $url, @@ -108,51 +84,38 @@ public function next() $this->request->getContent() ); } else { - $this->request = NULL; + $this->request = null; } - $this->response = NULL; + $this->response = null; $this->counter++; } - private function load() + private function load(): void { - if ($this->response === NULL) { + if ($this->response === null) { $this->response = $this->api->request($this->request); } } - /** - * @param string - * @return int - */ - public static function parsePage($url) + public static function parsePage(string $url): int { - list (, $parametersStr) = explode('?', $url, 2) + ['', '']; + [, $parametersStr] = explode('?', $url, 2) + ['', '']; parse_str($parametersStr, $parameters); - - return isset($parameters['page']) - ? max(1, (int) $parameters['page']) - : 1; + return max((int) ($parameters['page'] ?? 1), 1); } /** * @see https://developer.github.com/guides/traversing-with-pagination/#navigating-through-the-pages - * - * @param string - * @param string - * @return string|NULL */ - public static function parseLink($link, $rel) + public static function parseLink(string $link, string $rel): ?string { if (!preg_match('(<([^>]+)>;\s*rel="' . preg_quote($rel) . '")', $link, $match)) { - return NULL; + return null; } - return $match[1]; } - } diff --git a/src/Github/Sanity.php b/src/Github/Sanity.php deleted file mode 100644 index bb432f1..0000000 --- a/src/Github/Sanity.php +++ /dev/null @@ -1,28 +0,0 @@ -filePath($key), @@ -61,11 +60,7 @@ public function save($key, $value) } - /** - * @param string - * @return mixed|NULL - */ - public function load($key) + public function load(string $key): mixed { $path = $this->filePath($key); if (is_file($path) && ($fd = fopen($path, 'rb')) && flock($fd, LOCK_SH)) { @@ -73,8 +68,8 @@ public function load($key) flock($fd, LOCK_UN); fclose($fd); - $success = TRUE; - set_error_handler(function() use (& $success) { return $success = FALSE; }, E_NOTICE); + $success = true; + set_error_handler(function() use (&$success) { return $success = false; }, E_NOTICE); $cached = unserialize($cached); restore_error_handler(); @@ -82,16 +77,13 @@ public function load($key) return $cached; } } + + return null; } - /** - * @param string - * @return string - */ - private function filePath($key) + private function filePath(string $key): string { return $this->dir . DIRECTORY_SEPARATOR . sha1($key) . '.php'; } - } diff --git a/src/Github/Storages/ICache.php b/src/Github/Storages/ICache.php index 9540bbf..262b1cd 100644 --- a/src/Github/Storages/ICache.php +++ b/src/Github/Storages/ICache.php @@ -1,22 +1,17 @@ sessionKey = $sessionKey; - } + public function __construct( + private string $sessionKey = self::SESSION_KEY + ) {} - /** - * @param string - * @param mixed - * @return self - */ - public function set($name, $value) + public function set(string $name, mixed $value): static { - if ($value === NULL) { + if ($value === null) { return $this->remove($name); } @@ -45,42 +37,25 @@ public function set($name, $value) } - /** - * @param string - * @return mixed - */ - public function get($name) + public function get(string $name): mixed { $this->check(__METHOD__); - - return isset($_SESSION[$this->sessionKey][$name]) - ? $_SESSION[$this->sessionKey][$name] - : NULL; + return $_SESSION[$this->sessionKey][$name] ?? null; } - /** - * @param string - * @return self - */ - public function remove($name) + public function remove(string $name): static { $this->check(__METHOD__); - unset($_SESSION[$this->sessionKey][$name]); - return $this; } - /** - * @param string - */ - private function check($method) + private function check(string $method): void { if (!isset($_SESSION)) { trigger_error("Start session before using $method().", E_USER_WARNING); } } - } diff --git a/src/Github/Strict.php b/src/Github/Strict.php new file mode 100644 index 0000000..3cfc578 --- /dev/null +++ b/src/Github/Strict.php @@ -0,0 +1,25 @@ +response = clone $response; } - /** - * @return Http\Response|NULL - */ - final public function getResponse() + final public function getResponse(): ?Http\Response { return $this->response; } - } @@ -133,9 +126,7 @@ class InvalidResponseException extends ApiException * JSON cannot be decoded, or value cannot be encoded to JSON. */ class JsonException extends RuntimeException - { - } - + {} } @@ -148,7 +139,6 @@ class JsonException extends RuntimeException */ class BadResponseException extends Github\RuntimeException {} - } @@ -160,7 +150,6 @@ class BadResponseException extends Github\RuntimeException */ class LoginException extends Github\RuntimeException {} - } @@ -172,5 +161,4 @@ class LoginException extends Github\RuntimeException */ class MissingDirectoryException extends Github\RuntimeException {} - } diff --git a/src/ca-chain.crt b/src/ca-chain.crt index c95b005..9a0aa33 100644 --- a/src/ca-chain.crt +++ b/src/ca-chain.crt @@ -1,3 +1,58 @@ +# +# DigiCert Global Root CA +# +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +# +# DigiCert TLS Hybrid ECC SHA384 2020 CA1 +# +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIQCidf5wTW7ssj1c1bSxpOBDANBgkqhkiG9w0BAQwFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0yMDA5MjMwMDAwMDBaFw0zMDA5MjIyMzU5NTlaMFYxCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxMDAuBgNVBAMTJ0RpZ2lDZXJ0IFRMUyBI +eWJyaWQgRUNDIFNIQTM4NCAyMDIwIENBMTB2MBAGByqGSM49AgEGBSuBBAAiA2IA +BMEbxppbmNmkKaDp1AS12+umsmxVwP/tmMZJLwYnUcu/cMEFesOxnYeJuq20ExfJ +qLSDyLiQ0cx0NTY8g3KwtdD3ImnI8YDEe0CPz2iHJlw5ifFNkU3aiYvkA8ND5b8v +c6OCAa4wggGqMB0GA1UdDgQWBBQKvAgpF4ylOW16Ds4zxy6z7fvDejAfBgNVHSME +GDAWgBQD3lA1VtFMu2bwo+IbG8OXsj3RVTAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0l +BBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBIGA1UdEwEB/wQIMAYBAf8CAQAwdgYI +KwYBBQUHAQEEajBoMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j +b20wQAYIKwYBBQUHMAKGNGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp +Q2VydEdsb2JhbFJvb3RDQS5jcnQwewYDVR0fBHQwcjA3oDWgM4YxaHR0cDovL2Ny +bDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNybDA3oDWgM4Yx +aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNy +bDAwBgNVHSAEKTAnMAcGBWeBDAEBMAgGBmeBDAECATAIBgZngQwBAgIwCAYGZ4EM +AQIDMA0GCSqGSIb3DQEBDAUAA4IBAQDeOpcbhb17jApY4+PwCwYAeq9EYyp/3YFt +ERim+vc4YLGwOWK9uHsu8AjJkltz32WQt960V6zALxyZZ02LXvIBoa33llPN1d9R +JzcGRvJvPDGJLEoWKRGC5+23QhST4Nlg+j8cZMsywzEXJNmvPlVv/w+AbxsBCMqk +BGPI2lNM8hkmxPad31z6n58SXqJdH/bYF462YvgdgbYKOytobPAyTgr3mYI5sUje +CzqJx1+NLyc8nAK8Ib2HxnC+IrrWzfRLvVNve8KaN9EtBH7TuMwNW4SpDCmGr6fY +1h3tDjHhkTb9PA36zoaJzu0cIw265vZt6hCmYWJC+/j+fgZwcPwL +-----END CERTIFICATE----- + # # DigiCert High Assurance EV Root CA # diff --git a/src/github-api.php b/src/github-api.php index 6524c5d..1d4195a 100644 --- a/src/github-api.php +++ b/src/github-api.php @@ -1,7 +1,9 @@ decode($response)); $e = Assert::exception(function() use ($api) { $response = new Milo\Github\Http\Response(200, ['Content-Type' => 'application/json'], '['); $api->decode($response); -}, 'Milo\Github\InvalidResponseException', 'JSON decoding failed.'); +}, Milo\Github\InvalidResponseException::class, 'JSON decoding failed.'); $e = Assert::exception(function() use ($e) { throw $e->getPrevious(); -}, 'Milo\Github\JsonException'); +}, Milo\Github\JsonException::class); Assert::null($e->getPrevious()); @@ -33,63 +35,63 @@ Assert::null($e->getPrevious()); $e = Assert::exception(function() use ($api) { $response = new Milo\Github\Http\Response(200, ['Content-Type' => 'application/json'], '""'); $api->decode($response); -}, 'Milo\Github\InvalidResponseException', 'Decoded JSON is not an array or object.'); +}, Milo\Github\InvalidResponseException::class, 'Decoded JSON is not an array or object.'); Assert::null($e->getPrevious()); $e = Assert::exception(function() use ($api) { $response = new Milo\Github\Http\Response(400, ['Content-Type' => 'application/json'], '{"message":"error"}'); $api->decode($response); -}, 'Milo\Github\BadRequestException', 'error', 400); +}, Milo\Github\BadRequestException::class, 'error', 400); Assert::null($e->getPrevious()); $e = Assert::exception(function() use ($api) { $response = new Milo\Github\Http\Response(401, ['Content-Type' => 'application/json'], '{"message":"error"}'); $api->decode($response); -}, 'Milo\Github\UnauthorizedException', 'error', 401); +}, Milo\Github\UnauthorizedException::class, 'error', 401); Assert::null($e->getPrevious()); $e = Assert::exception(function() use ($api) { $response = new Milo\Github\Http\Response(403, ['Content-Type' => 'application/json'], '{"message":"error"}'); $api->decode($response); -}, 'Milo\Github\ForbiddenException', 'error', 403); +}, Milo\Github\ForbiddenException::class, 'error', 403); Assert::null($e->getPrevious()); $e = Assert::exception(function() use ($api) { $response = new Milo\Github\Http\Response(403, ['Content-Type' => 'application/json', 'X-RateLimit-Remaining' => '0'], '{"message":"error"}'); $api->decode($response); -}, 'Milo\Github\RateLimitExceedException', 'error', 403); +}, Milo\Github\RateLimitExceedException::class, 'error', 403); Assert::null($e->getPrevious()); $e = Assert::exception(function() use ($api) { $response = new Milo\Github\Http\Response(404, [], ''); $api->decode($response); -}, 'Milo\Github\NotFoundException', 'Resource not found or not authorized to access.', 404); +}, Milo\Github\NotFoundException::class, 'Resource not found or not authorized to access.', 404); Assert::null($e->getPrevious()); $e = Assert::exception(function() use ($api) { $response = new Milo\Github\Http\Response(422, ['Content-Type' => 'application/json'], '{"message":"error"}'); $api->decode($response); -}, 'Milo\Github\UnprocessableEntityException', 'error', 422); +}, Milo\Github\UnprocessableEntityException::class, 'error', 422); Assert::null($e->getPrevious()); $e = Assert::exception(function() use ($api) { $response = new Milo\Github\Http\Response(422, ['Content-Type' => 'application/json'], '{"message":"error", "errors":[{"a":"b","c":"d"}]}'); $api->decode($response); -}, 'Milo\Github\UnprocessableEntityException', 'error[b:d]', 422); +}, Milo\Github\UnprocessableEntityException::class, 'error[b:d]', 422); Assert::null($e->getPrevious()); $e = Assert::exception(function() use ($api) { $response = new Milo\Github\Http\Response(500, [], ''); $api->decode($response); -}, 'Milo\Github\UnexpectedResponseException', 'Expected response with code < 300.', 500); +}, Milo\Github\UnexpectedResponseException::class, 'Expected response with code < 300.', 500); Assert::null($e->getPrevious()); @@ -99,5 +101,5 @@ Assert::same('foo', $api->decode($response, [500])); $e = Assert::exception(function() use ($api) { $response = new Milo\Github\Http\Response(200, [], ''); $api->decode($response, [201, 304]); -}, 'Milo\Github\UnexpectedResponseException', 'Expected response with code 201 or 304.', 200); +}, Milo\Github\UnexpectedResponseException::class, 'Expected response with code 201 or 304.', 200); Assert::null($e->getPrevious()); diff --git a/tests/Github/Api.expandColonParameters.phpt b/tests/Github/Api.expandColonParameters.phpt index e691627..b24460f 100644 --- a/tests/Github/Api.expandColonParameters.phpt +++ b/tests/Github/Api.expandColonParameters.phpt @@ -1,5 +1,7 @@ '?a=A&b=B', @@ -28,6 +30,8 @@ test(function() { ':a/' => 'A?b=B', '/:a/' => '/A?b=B', '/:a/:b/c' => '/A/B/c', + '/:a.:b/c' => '/A.B/c', + '/:a...:b/c' => '/A...B/c', ]; foreach ($urls as $url => $expected) { @@ -36,17 +40,17 @@ test(function() { Assert::exception(function() use ($api) { $api->expandColonParameters(':a', ['A' => 'a']); - }, 'Milo\Github\MissingParameterException', "Missing parameter 'a' for URL path ':a'."); + }, Milo\Github\MissingParameterException::class, "Missing parameter 'a' for URL path ':a'."); Assert::exception(function() use ($api) { $api->expandColonParameters(':a:b', ['a' => 'A', 'b' => 'B']); - }, 'Milo\Github\MissingParameterException', "Missing parameter 'a:b' for URL path ':a:b'."); + }, Milo\Github\MissingParameterException::class, "Missing parameter 'a:b' for URL path ':a:b'."); }); # Parameters escaping test(function() { - $api = new TestApi; + $api = new TestExpandColonApi; Assert::same('/with%20space', $api->expandColonParameters('/:name', ['name' => 'with space'])); }); @@ -54,8 +58,16 @@ test(function() { # Default parameters expanding test(function() { - $api = new TestApi; + $api = new TestExpandColonApi; Assert::same('/default', $api->expandColonParameters('/:var', [], ['var' => 'default'])); Assert::same('/set', $api->expandColonParameters('/:var', ['var' => 'set'], ['var' => 'default'])); }); + + +# Expanding in absolute URL +test(function () { + $api = new Milo\Github\Api; + $request = $api->createRequest('', 'https://host.:name.:tld/path/:name', ['name' => 'milo', 'tld' => 'cz']); + Assert::same('https://host.:name.:tld/path/milo?tld=cz', $request->getUrl()); +}); diff --git a/tests/Github/Api.expandUriTemplate.phpt b/tests/Github/Api.expandUriTemplate.phpt index 7029ee2..2aa436f 100644 --- a/tests/Github/Api.expandUriTemplate.phpt +++ b/tests/Github/Api.expandUriTemplate.phpt @@ -1,5 +1,7 @@ '', ]; foreach ($cases as $operator => $expected) { - Assert::same($expected, $api->expandUriTemplate("{{$operator}a}", ['a' => NULL])); + Assert::same($expected, $api->expandUriTemplate("{{$operator}a}", ['a' => null])); } @@ -46,7 +48,7 @@ $cases = [ ]; foreach ($cases as $operator => $expected) { Assert::same($expected, $api->expandUriTemplate("{{$operator}a}", ['a' => ''])); - Assert::same($expected, $api->expandUriTemplate("{{$operator}a}", ['a' => FALSE])); + Assert::same($expected, $api->expandUriTemplate("{{$operator}a}", ['a' => false])); } @@ -328,7 +330,7 @@ test(function() use ($api) { 'y' => 768, 'empty' => '', 'empty_keys' => [], - 'undef' => NULL, + 'undef' => null, ]; $cases = [ # 3.2.1. Variable Expansion @@ -469,3 +471,11 @@ test(function() use ($api) { Assert::same($expected, $api->expandUriTemplate($template, $parameters)); } }); + + +# Expanding in absolute URL +test(function () { + $api = new Milo\Github\Api; + $request = $api->createRequest('', 'https://host.{name}.{tld}/path/{name}', ['name' => 'milo', 'tld' => 'cz']); + Assert::same('https://host.{name}.{tld}/path/milo', $request->getUrl()); +}); diff --git a/tests/Github/Api.phpt b/tests/Github/Api.phpt index c0f8d26..9280b7e 100644 --- a/tests/Github/Api.phpt +++ b/tests/Github/Api.phpt @@ -1,5 +1,7 @@ getClient()); @@ -29,18 +40,25 @@ test(function() { Assert::same($api, $api->setUrl('url://test')); Assert::same('url://test', $api->getUrl()); + $clone = $api->withUrl('url://cloned'); + Assert::notSame($api, $clone); + Assert::same('url://test', $api->getUrl()); + Assert::same('url://cloned', $clone->getUrl()); + Assert::same($api->getClient(), $clone->getClient()); + $token = new Milo\Github\OAuth\Token('hash'); Assert::null($api->getToken()); Assert::same($api, $api->setToken($token)); Assert::same($token, $api->getToken()); - $api->setToken(NULL); + $api->setToken(null); Assert::null($api->getToken()); + }); # createRequest() test(function() { - $client = new MockIClient; + $client = new LogicExceptionMockClient; $api = new Milo\Github\Api($client); # All slashes in URL @@ -52,7 +70,7 @@ test(function() { # No slashes in URL $api->setUrl('url://test'); - $request = $api->createRequest('', 'path', [], [], NULL); + $request = $api->createRequest('', 'path', [], [], null); Assert::same('url://test/path', $request->getUrl()); # Array to JSON @@ -60,7 +78,7 @@ test(function() { Assert::same(['content-type' => 'application/json; charset=utf-8'], $request->getHeaders()); Assert::same('{"foo":"bar"}', $request->getContent()); - ## Object to JSON + # Object to JSON $request = $api->createRequest('', '', [], [], (object) ['foo' => 'bar']); Assert::same(['content-type' => 'application/json; charset=utf-8'], $request->getHeaders()); Assert::same('{"foo":"bar"}', $request->getContent()); @@ -69,48 +87,58 @@ test(function() { # Default parameters test(function() { - $client = new MockIClient; + $client = new LogicExceptionMockClient; $api = new Milo\Github\Api($client); $api->setUrl('url://test'); Assert::same([], $api->getDefaultParameters()); - $request = $api->createRequest('', 'path', [], [], NULL); + $request = $api->createRequest('', 'path', [], [], null); Assert::same('url://test/path', $request->getUrl()); $params = ['foo' => 'bar']; $api->setDefaultParameters($params); Assert::same($params, $api->getDefaultParameters()); - $request = $api->createRequest('', 'path', [], [], NULL); + $request = $api->createRequest('', 'path', [], [], null); Assert::same('url://test/path?foo=bar', $request->getUrl()); Assert::same($params, $api->getDefaultParameters()); - $request = $api->createRequest('', 'path', ['foo' => 'fuzz'], [], NULL); + $request = $api->createRequest('', 'path', ['foo' => 'fuzz'], [], null); Assert::same('url://test/path?foo=fuzz', $request->getUrl()); }); -# Api called with full URL +# Api called with absolute URL test(function() { - $client = new MockIClient; + $client = new LogicExceptionMockClient; $api = new Milo\Github\Api($client); $api->setUrl('url://test'); - $request = $api->createRequest('', 'url://test/path', [], [], NULL); + $request = $api->createRequest('', 'url://test/path', [], [], null); Assert::same('url://test/path', $request->getUrl()); - $request = $api->createRequest('', 'url://tested/path', [], [], NULL); + $request = $api->createRequest('', 'url://tested/path', [], [], null); Assert::same('url://test/ed/path', $request->getUrl()); - $request = $api->createRequest('', 'uRl://TeSt/path', [], [], NULL); + $request = $api->createRequest('', 'uRl://TeSt/path', [], [], null); Assert::same('url://test/path', $request->getUrl()); + + # Absolute HTTPS URL with different host + $request = $api->createRequest('', 'https://example.com', [], [], null); + Assert::same('https://example.com/', $request->getUrl()); + $request = $api->createRequest('', 'https://example.com/path', [], [], null); + Assert::same('https://example.com/path', $request->getUrl()); + + # Absolute non-HTTPS URL with different host is not allowed (should be?) + $request = $api->createRequest('', 'http://example.com', [], [], null); + Assert::same('url://test/http://example.com', $request->getUrl()); }); # Paginator test(function() { - $client = new MockIClient; + $client = new LogicExceptionMockClient; $api = new Milo\Github\Api($client); - Assert::type('Milo\Github\Paginator', $api->paginator('')); + Assert::type(Milo\Github\Paginator::class, $api->paginator('')); }); diff --git a/tests/Github/Api.request.phpt b/tests/Github/Api.request.phpt index b1e28c8..8416c56 100644 --- a/tests/Github/Api.request.phpt +++ b/tests/Github/Api.request.phpt @@ -1,5 +1,7 @@ onRequest, $request); } - public function onRequest($cb) {} - public function onResponse($cb) {} + + public function onRequest(?callable $callback): static + { + return $this; + } + + + public function onResponse(?callable $callback): static + { + return $this; + } } @@ -35,6 +46,7 @@ $client->onRequest = function(Milo\Github\Http\Request $request) { Assert::true($request->hasHeader('User-Agent')); Assert::true($request->hasHeader('Foo')); Assert::false($request->hasHeader('Authorization')); + return new Milo\Github\Http\Response(0, [], null); }; $api->request($request); @@ -44,14 +56,16 @@ $token = new Milo\Github\OAuth\Token('hash', 'type', []); $api->setToken($token); $client->onRequest = function(Milo\Github\Http\Request $request) { Assert::true($request->hasHeader('Authorization')); + return new Milo\Github\Http\Response(0, [], null); }; $api->request($request); # Without token again -$api->setToken(NULL); +$api->setToken(null); $client->onRequest = function(Milo\Github\Http\Request $request) { Assert::false($request->hasHeader('Authorization')); + return new Milo\Github\Http\Response(0, [], null); }; $api->request($request); @@ -61,6 +75,7 @@ $client->onRequest = function(Milo\Github\Http\Request $request) { Assert::same('DELETE', $request->getMethod()); Assert::same('https://api.github.com/url?a=b', $request->getUrl()); Assert::same('bar', $request->getHeader('Foo')); + return new Milo\Github\Http\Response(0, [], null); }; $api->delete('/url', ['a' => 'b'], ['Foo' => 'bar']); @@ -70,6 +85,7 @@ $client->onRequest = function(Milo\Github\Http\Request $request) { Assert::same('GET', $request->getMethod()); Assert::same('https://api.github.com/url?a=b', $request->getUrl()); Assert::same('bar', $request->getHeader('Foo')); + return new Milo\Github\Http\Response(0, [], null); }; $api->get('/url', ['a' => 'b'], ['Foo' => 'bar']); @@ -79,6 +95,7 @@ $client->onRequest = function(Milo\Github\Http\Request $request) { Assert::same('HEAD', $request->getMethod()); Assert::same('https://api.github.com/url?a=b', $request->getUrl()); Assert::same('bar', $request->getHeader('Foo')); + return new Milo\Github\Http\Response(0, [], null); }; $api->head('/url', ['a' => 'b'], ['Foo' => 'bar']); @@ -89,6 +106,7 @@ $client->onRequest = function(Milo\Github\Http\Request $request) { Assert::same('https://api.github.com/url?a=b', $request->getUrl()); Assert::same('bar', $request->getHeader('Foo')); Assert::same('{content}', $request->getContent()); + return new Milo\Github\Http\Response(0, [], null); }; $api->patch('/url', '{content}', ['a' => 'b'], ['Foo' => 'bar']); @@ -99,6 +117,7 @@ $client->onRequest = function(Milo\Github\Http\Request $request) { Assert::same('https://api.github.com/url?a=b', $request->getUrl()); Assert::same('bar', $request->getHeader('Foo')); Assert::same('{content}', $request->getContent()); + return new Milo\Github\Http\Response(0, [], null); }; $api->post('/url', '{content}', ['a' => 'b'], ['Foo' => 'bar']); @@ -109,6 +128,7 @@ $client->onRequest = function(Milo\Github\Http\Request $request) { Assert::same('https://api.github.com/url?a=b', $request->getUrl()); Assert::same('bar', $request->getHeader('Foo')); Assert::same('{content}', $request->getContent()); + return new Milo\Github\Http\Response(0, [], null); }; $api->put('/url', '{content}', ['a' => 'b'], ['Foo' => 'bar']); diff --git a/tests/Github/Helpers.phpt b/tests/Github/Helpers.phpt index 9941020..7375f48 100644 --- a/tests/Github/Helpers.phpt +++ b/tests/Github/Helpers.phpt @@ -1,5 +1,7 @@ onProcess, $request); } @@ -30,8 +32,8 @@ test(function() { new Milo\Github\Http\Response(200, ['Location' => ''], ''), ]; - $client = new TestClient; - $client->onProcess = function (Milo\Github\Http\Request $request) use (& $responses) { + $client = new TestAbstractClient; + $client->onProcess = function (Milo\Github\Http\Request $request) use (&$responses) { return array_shift($responses); }; @@ -54,16 +56,16 @@ test(function() { # onRequest(), onResponse() test(function() { - $client = new TestClient; + $client = new TestAbstractClient; $client->onProcess = function() { return new Milo\Github\Http\Response(200, [], '{response}'); }; - $onRequest = NULL; - $client->onRequest(function(Milo\Github\Http\Request $request) use (& $onRequest) { + $onRequest = null; + $client->onRequest(function(Milo\Github\Http\Request $request) use (&$onRequest) { $onRequest = $request->getContent(); }); - $onResponse = NULL; - $client->onResponse(function(Milo\Github\Http\Response $response) use (& $onResponse) { + $onResponse = null; + $client->onResponse(function(Milo\Github\Http\Response $response) use (&$onResponse) { $onResponse = $response->getContent(); }); @@ -76,7 +78,7 @@ test(function() { # Additional headers test(function(){ - $client = new TestClient; + $client = new TestAbstractClient; $client->onProcess = function(Milo\Github\Http\Request $request) { Assert::same([ 'expect' => '', diff --git a/tests/Github/Http/CachedClient.phpt b/tests/Github/Http/CachedClient.phpt index bc6001b..37631cb 100644 --- a/tests/Github/Http/CachedClient.phpt +++ b/tests/Github/Http/CachedClient.phpt @@ -1,5 +1,7 @@ onRequest, $request); $this->requestCount++; return $response; } - public function onRequest($foo) + public function onRequest(?callable $callback): static { - trigger_error('Inner onRequest called: ' . var_export($foo, TRUE), E_USER_NOTICE); + trigger_error('Inner onRequest called: ' . var_export($callback, true), E_USER_NOTICE); + return $this; } - public function onResponse($foo) + public function onResponse(?callable $callback): static { - trigger_error('Inner onResponse called: ' . var_export($foo, TRUE), E_USER_NOTICE); + trigger_error('Inner onResponse called: ' . var_export($callback, true), E_USER_NOTICE); + return $this; } - } class MockCache implements Milo\Github\Storages\ICache { - private $cache = []; + private array $cache = []; - public function save($key, $value) { + public function save(string $key, mixed $value): mixed + { return $this->cache[$key] = $value; } - public function load($key) + public function load(string $key): mixed { - return isset($this->cache[$key]) - ? $this->cache[$key] - : NULL; + return $this->cache[$key] ?? null; } - } class CachingTestCase extends Tester\TestCase { - /** @var Http\CachedClient */ - private $client; + private Http\CachedClient $client; - /** @var MockClient */ - private $innerClient; + private MockClientCounter $innerClient; public function setup() { $cache = new MockCache; - $this->innerClient = new MockClient; + $this->innerClient = new MockClientCounter; $this->client = new Http\CachedClient($cache, $this->innerClient); $this->innerClient->onRequest = function (Http\Request $request) { @@ -86,18 +84,20 @@ class CachingTestCase extends Tester\TestCase { Assert::same($this->innerClient, $this->client->getInnerClient()); - Assert::error(function() { - Assert::same($this->client, $this->client->onRequest('callback-1')); - Assert::same($this->client, $this->client->onResponse('callback-2')); + $cb = fn() => null; + + Assert::error(function() use ($cb) { + Assert::same($this->client, $this->client->onRequest($cb)); + Assert::same($this->client, $this->client->onResponse($cb)); }, [ - [E_USER_NOTICE, "Inner onRequest called: 'callback-1'"], + [E_USER_NOTICE, 'Inner onRequest called: %a?%Closure::%A%'], [E_USER_NOTICE, 'Inner onResponse called: NULL'], ]); - $onResponseCalled = FALSE; - Assert::error(function() use (& $onResponseCalled) { - $this->client->onResponse(function() use (& $onResponseCalled) { - $onResponseCalled = TRUE; + $onResponseCalled = false; + Assert::error(function() use (&$onResponseCalled) { + $this->client->onResponse(function() use (&$onResponseCalled) { + $onResponseCalled = true; }); }, E_USER_NOTICE, 'Inner onResponse called: NULL'); @@ -149,7 +149,7 @@ class CachingTestCase extends Tester\TestCase }; $response = $this->client->request(new Http\Request('', '', [], '2')); Assert::same('response-1', $response->getContent()); - Assert::type('Milo\Github\Http\Response', $response->getPrevious()); + Assert::type(Milo\Github\Http\Response::class, $response->getPrevious()); Assert::same(304, $response->getPrevious()->getCode()); Assert::same(2, $this->innerClient->requestCount); } @@ -178,7 +178,7 @@ class CachingTestCase extends Tester\TestCase $response = $this->client->request(new Http\Request('', '', [], '2')); Assert::same('response-1', $response->getContent()); - Assert::type('Milo\Github\Http\Response', $response->getPrevious()); + Assert::type(Milo\Github\Http\Response::class, $response->getPrevious()); Assert::same(304, $response->getPrevious()->getCode()); Assert::same(2, $this->innerClient->requestCount); } @@ -207,7 +207,7 @@ class CachingTestCase extends Tester\TestCase $response = $this->client->request(new Http\Request('', '', [], '2')); Assert::same('response-1', $response->getContent()); - Assert::type('Milo\Github\Http\Response', $response->getPrevious()); + Assert::type(Milo\Github\Http\Response::class, $response->getPrevious()); Assert::same(304, $response->getPrevious()->getCode()); Assert::same(2, $this->innerClient->requestCount); } @@ -226,14 +226,14 @@ class CachingTestCase extends Tester\TestCase # From cache $response = $this->client->request($request); Assert::same('inner-200-same', $response->getContent()); - Assert::type('Milo\Github\Http\Response', $response->getPrevious()); + Assert::type(Milo\Github\Http\Response::class, $response->getPrevious()); Assert::same('inner-304-same', $response->getPrevious()->getContent()); Assert::same(2, $this->innerClient->requestCount); # Again $response = $this->client->request($request); Assert::same('inner-200-same', $response->getContent()); - Assert::type('Milo\Github\Http\Response', $response->getPrevious()); + Assert::type(Milo\Github\Http\Response::class, $response->getPrevious()); Assert::same('inner-304-same', $response->getPrevious()->getContent()); Assert::same(3, $this->innerClient->requestCount); } @@ -250,13 +250,13 @@ class CachingTestCase extends Tester\TestCase $response = $this->client->request($request); Assert::same('inner-200-disabled', $response->getContent()); - Assert::type('Milo\Github\Http\Response', $response->getPrevious()); + Assert::type(Milo\Github\Http\Response::class, $response->getPrevious()); Assert::same('inner-304-disabled', $response->getPrevious()->getContent()); Assert::same(2, $this->innerClient->requestCount); $response = $this->client->request($request); Assert::same('inner-200-disabled', $response->getContent()); - Assert::type('Milo\Github\Http\Response', $response->getPrevious()); + Assert::type(Milo\Github\Http\Response::class, $response->getPrevious()); Assert::same('inner-304-disabled', $response->getPrevious()->getContent()); Assert::same(3, $this->innerClient->requestCount); } @@ -264,7 +264,7 @@ class CachingTestCase extends Tester\TestCase public function testForbidRecheckEnabled() { - $this->client = new Http\CachedClient(new MockCache, $this->innerClient, TRUE); + $this->client = new Http\CachedClient(new MockCache, $this->innerClient, true); $request = new Http\Request('', '', [], 'enabled'); @@ -283,7 +283,6 @@ class CachingTestCase extends Tester\TestCase Assert::null($response->getPrevious()); Assert::same(1, $this->innerClient->requestCount); } - } (new CachingTestCase)->run(); diff --git a/tests/Github/Http/Message.phpt b/tests/Github/Http/Message.phpt index d606eb0..d754274 100644 --- a/tests/Github/Http/Message.phpt +++ b/tests/Github/Http/Message.phpt @@ -1,5 +1,7 @@ getHeader('Set')); Assert::same($message, $message->setHeader('a', 'val')); Assert::same('val', $message->getHeader('a')); -Assert::same($message, $message->setHeader('a', NULL)); +Assert::same($message, $message->setHeader('a', null)); Assert::false($message->hasHeader('a')); # Content -Assert::same(NULL, (new TestMessage([], NULL))->getContent()); +Assert::same(null, (new TestMessage([], null))->getContent()); Assert::same('', (new TestMessage([], ''))->getContent()); diff --git a/tests/Github/Http/Request.phpt b/tests/Github/Http/Request.phpt index 8742c2e..73f6509 100644 --- a/tests/Github/Http/Request.phpt +++ b/tests/Github/Http/Request.phpt @@ -1,5 +1,7 @@ getCode()); Assert::true($response->isCode(200)); -Assert::true($response->isCode('200')); Assert::false($response->isCode(0)); # Previous -$response = new Milo\Github\Http\Response('200', [], '1'); -$previous = new Milo\Github\Http\Response('200', [], '2'); +$response = new Milo\Github\Http\Response(200, [], '1'); +$previous = new Milo\Github\Http\Response(200, [], '2'); Assert::null($response->getPrevious()); $response->setPrevious($previous); @@ -25,4 +26,4 @@ Assert::same($previous, $response->getPrevious()); Assert::exception(function() use ($response, $previous) { $response->setPrevious($previous); -}, 'Milo\Github\LogicException', 'Previous response is already set.'); +}, Milo\Github\LogicException::class, 'Previous response is already set.'); diff --git a/tests/Github/Http/StreamClient.phpt b/tests/Github/Http/StreamClient.phpt index 4e27a82..37e6307 100644 --- a/tests/Github/Http/StreamClient.phpt +++ b/tests/Github/Http/StreamClient.phpt @@ -1,5 +1,7 @@ onFileGetContents, $url, $contextOptions); } + + + public function onRequest(?callable $callback): static + { + return $this; + } + + + public function onResponse(?callable $callback): static + { + return $this; + } } # Common test(function() { - $client = new TestClient; + $client = new TestStreamClient; $client->onFileGetContents = function($url, array $contextOptions) { Assert::same('http://example.com', $url); Assert::same([ @@ -31,13 +45,13 @@ test(function() { 'header' => "custom: header\r\nexpect: \r\nconnection: close\r\n", 'follow_location' => 0, 'protocol_version' => 1.1, - 'ignore_errors' => TRUE, + 'ignore_errors' => true, 'content' => '{content}', ], 'ssl' => [ - 'verify_peer' => TRUE, + 'verify_peer' => true, 'cafile' => realpath(__DIR__ . '/../../../src/ca-chain.crt'), - 'disable_compression' => TRUE, + 'disable_compression' => true, ], ], $contextOptions); @@ -54,14 +68,14 @@ test(function() { # SSL options test(function() { - $client = new TestClient(['option' => 'value']); + $client = new TestStreamClient(['option' => 'value']); $client->onFileGetContents = function($url, array $contextOptions) { Assert::type('array', $contextOptions['ssl']); Assert::same([ 'option' => 'value', - 'verify_peer' => TRUE, + 'verify_peer' => true, 'cafile' => realpath(__DIR__ . '/../../../src/ca-chain.crt'), - 'disable_compression' => TRUE, + 'disable_compression' => true, ], $contextOptions['ssl']); return [200, [], '']; diff --git a/tests/Github/OAuth/Configuration.phpt b/tests/Github/OAuth/Configuration.phpt index 6e40d20..b3b14b6 100644 --- a/tests/Github/OAuth/Configuration.phpt +++ b/tests/Github/OAuth/Configuration.phpt @@ -1,5 +1,7 @@ onRequest, $request); } - public function onRequest($cb) {} - public function onResponse($cb) {} + + public function onRequest(?callable $callback): static + { + return $this; + } + + + public function onResponse(?callable $callback): static + { + return $this; + } } class LoginTestCase extends Tester\TestCase { - /** @var Milo\Github\OAuth\Configuration */ - private $config; - - /** @var Milo\Github\Storages\SessionStorage */ - private $storage; + private Milo\Github\Storages\SessionStorage $storage; - /** @var MockClient */ - private $client; + private MockClient $client; - /** @var Milo\Github\OAuth\Login */ - private $login; + private Milo\Github\OAuth\Login $login; public function setup() @@ -58,7 +63,7 @@ class LoginTestCase extends Tester\TestCase Assert::false($this->login->hasToken()); Assert::exception(function() { $this->login->getToken(); - }, 'Milo\Github\LogicException', 'Token has not been obtained yet.'); + }, Milo\Github\LogicException::class, 'Token has not been obtained yet.'); } @@ -81,7 +86,7 @@ class LoginTestCase extends Tester\TestCase Assert::same(['a','b','c'], $token->getScopes()); Assert::true($this->login->hasToken()); - Assert::type('Milo\Github\OAuth\Token', $this->login->getToken()); + Assert::type(Milo\Github\OAuth\Token::class, $this->login->getToken()); Assert::same($this->login, $this->login->dropToken()); Assert::false($this->login->hasToken()); @@ -106,7 +111,7 @@ class LoginTestCase extends Tester\TestCase Assert::exception(function() { $this->login->obtainToken('', ''); - }, 'Milo\Github\OAuth\LoginException', 'OAuth security state does not match.'); + }, Milo\Github\OAuth\LoginException::class, 'OAuth security state does not match.'); } @@ -119,10 +124,10 @@ class LoginTestCase extends Tester\TestCase }; $e = Assert::exception(function() { $this->login->obtainToken('', '*****'); - }, 'Milo\Github\OAuth\LoginException', 'HTTP request failed.'); + }, Milo\Github\OAuth\LoginException::class, 'HTTP request failed.'); $e = Assert::exception(function() use ($e) { throw $e->getPrevious(); - }, 'Milo\Github\Http\BadResponseException', 'fail'); + }, Milo\Github\Http\BadResponseException::class, 'fail'); Assert::null($e->getPrevious()); } @@ -136,10 +141,10 @@ class LoginTestCase extends Tester\TestCase }; $e = Assert::exception(function() { $this->login->obtainToken('', '*****'); - }, 'Milo\Github\OAuth\LoginException', 'Bad JSON in response.'); + }, Milo\Github\OAuth\LoginException::class, 'Bad JSON in response.'); $e = Assert::exception(function() use ($e) { throw $e->getPrevious(); - }, 'Milo\Github\JsonException', 'Syntax error, malformed JSON'); + }, Milo\Github\JsonException::class, 'Syntax error'); Assert::null($e->getPrevious()); } @@ -153,7 +158,7 @@ class LoginTestCase extends Tester\TestCase }; $e = Assert::exception(function() { $this->login->obtainToken('', '*****'); - }, 'Milo\Github\OAuth\LoginException', 'fail'); + }, Milo\Github\OAuth\LoginException::class, 'fail'); Assert::null($e->getPrevious()); } @@ -166,10 +171,25 @@ class LoginTestCase extends Tester\TestCase }; $e = Assert::exception(function() { $this->login->obtainToken('', '*****'); - }, 'Milo\Github\OAuth\LoginException', 'Unexpected response.'); + }, Milo\Github\OAuth\LoginException::class, 'Unexpected response.'); Assert::null($e->getPrevious()); } + + /** @deprecated */ + public function testTokenObjectInSession() + { + $token = new Milo\Github\OAuth\Token('a', 'b', ['c', 'd']); + $this->storage->set('auth.token', $token); + + Assert::type(Milo\Github\OAuth\Token::class, $this->login->getToken()); + Assert::same([ + 'value' => 'a', + 'type' => 'b', + 'scopes' => ['c', 'd'], + ], $this->storage->get('auth.token')); + } + } (new LoginTestCase())->run(); diff --git a/tests/Github/OAuth/Token.phpt b/tests/Github/OAuth/Token.phpt index dd5bead..85dda9c 100644 --- a/tests/Github/OAuth/Token.phpt +++ b/tests/Github/OAuth/Token.phpt @@ -1,5 +1,7 @@ hasScope('user')); Assert::true($token->hasScope('user:email')); Assert::false($token->hasScope('user:foo')); Assert::false($token->hasScope('foo')); + +$asArray = [ + 'value' => 'hash2', + 'type' => 'type', + 'scopes' => ['user'], +]; +Assert::same($asArray, $token->toArray()); + +$fromArray = Milo\Github\OAuth\Token::createFromArray($asArray); +Assert::equal($token, $fromArray); diff --git a/tests/Github/Paginator.phpt b/tests/Github/Paginator.phpt index 1681e87..c9311cc 100644 --- a/tests/Github/Paginator.phpt +++ b/tests/Github/Paginator.phpt @@ -1,5 +1,7 @@ onRequest, $request); } @@ -24,8 +25,7 @@ class MockApi extends Milo\Github\Api class PaginatorTestCase extends Tester\TestCase { - /** @var MockApi */ - private $api; + private MockApi $api; public function setUp() { @@ -48,7 +48,7 @@ class PaginatorTestCase extends Tester\TestCase )); $requests = []; - $this->api->onRequest = function(Milo\Github\Http\Request $request) use (& $requests, & $responses) { + $this->api->onRequest = function(Milo\Github\Http\Request $request) use (&$requests, &$responses) { $requests[] = $request; return array_shift($responses); }; @@ -96,8 +96,8 @@ class PaginatorTestCase extends Tester\TestCase Assert::same('url://b', Milo\Github\Paginator::parseLink($link, 'b')); Assert::same('url://c', Milo\Github\Paginator::parseLink($link, 'c')); - Assert::same(NULL, Milo\Github\Paginator::parseLink('', '')); - Assert::same(NULL, Milo\Github\Paginator::parseLink('; rel="foo"', 'bar')); + Assert::same(null, Milo\Github\Paginator::parseLink('', '')); + Assert::same(null, Milo\Github\Paginator::parseLink('; rel="foo"', 'bar')); } @@ -114,7 +114,7 @@ class PaginatorTestCase extends Tester\TestCase 'url://test?page=20' )); - $this->api->onRequest = function(Milo\Github\Http\Request $request) use (& $requests, & $stack) { + $this->api->onRequest = function(Milo\Github\Http\Request $request) use (&$requests, &$stack) { $requests[] = $request; return array_shift($stack); }; diff --git a/tests/Github/Sanity.phpt b/tests/Github/Sanity.phpt deleted file mode 100644 index 239430b..0000000 --- a/tests/Github/Sanity.phpt +++ /dev/null @@ -1,20 +0,0 @@ -undefined; -}, 'Milo\Github\LogicException', 'Cannot read an undeclared property TestSanity::$undefined.'); - -Assert::exception(function() { - $mock = new TestSanity; - $mock->undefined = ''; -}, 'Milo\Github\LogicException', 'Cannot write to an undeclared property TestSanity::$undefined.'); diff --git a/tests/Github/Storages/FileCache.phpt b/tests/Github/Storages/FileCache.phpt index e97235f..0128df7 100644 --- a/tests/Github/Storages/FileCache.phpt +++ b/tests/Github/Storages/FileCache.phpt @@ -1,5 +1,7 @@ getPrevious()); -define('TEMP_DIR', __DIR__ . '/temp.FileCache'); +const TEMP_DIR = __DIR__ . '/temp.FileCache'; @mkdir(TEMP_DIR); # @ = directory may exist Tester\Helpers::purge(TEMP_DIR); @@ -22,13 +24,13 @@ $cache = new Milo\Github\Storages\FileCache(TEMP_DIR); Assert::null($cache->load('undefined')); -$value = $cache->save('key-1', NULL); +$value = $cache->save('key-1', null); Assert::null($cache->load('key-1')); -$value = $cache->save('key-2', TRUE); +$value = $cache->save('key-2', true); Assert::true($cache->load('key-2')); -$value = $cache->save('key-3', FALSE); +$value = $cache->save('key-3', false); Assert::false($cache->load('key-3')); $value = $cache->save('key-4', []); diff --git a/tests/Github/Storages/SessionStorage.phpt b/tests/Github/Storages/SessionStorage.phpt index 4906322..185658c 100644 --- a/tests/Github/Storages/SessionStorage.phpt +++ b/tests/Github/Storages/SessionStorage.phpt @@ -1,5 +1,7 @@ ['foo' => $o], ],$_SESSION); -Assert::same($storage, $storage->set('foo', NULL)); +Assert::same($storage, $storage->set('foo', null)); Assert::same([ 'test' => [], ], $_SESSION); diff --git a/tests/Github/Strict.phpt b/tests/Github/Strict.phpt new file mode 100644 index 0000000..fe51ba4 --- /dev/null +++ b/tests/Github/Strict.phpt @@ -0,0 +1,24 @@ +undefined; +}, Milo\Github\LogicException::class, 'Cannot read an undeclared property TestStrict::$undefined.'); + +Assert::exception(function() { + $mock = new TestStrict; + $mock->undefined = ''; +}, Milo\Github\LogicException::class, 'Cannot write to an undeclared property TestStrict::$undefined.'); diff --git a/tests/Github/loader.phpt b/tests/Github/loader.phpt index a89ff98..ca01d59 100644 --- a/tests/Github/loader.phpt +++ b/tests/Github/loader.phpt @@ -1,5 +1,7 @@