diff --git a/composer.json b/composer.json index aa74856..e509611 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,8 @@ "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^10.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.1", "squizlabs/php_codesniffer": "^3.5" }, "conflict": { diff --git a/src/Client.php b/src/Client.php index 6664889..5a19123 100644 --- a/src/Client.php +++ b/src/Client.php @@ -4,18 +4,9 @@ namespace SnapAuth; -use Composer\InstalledVersions; -use JsonException; use SensitiveParameter; use function assert; -use function curl_close; -use function curl_errno; -use function curl_exec; -use function curl_getinfo; -use function curl_init; -use function curl_setopt_array; -use function curl_version; use function is_array; use function is_string; use function json_decode; @@ -23,15 +14,6 @@ use function sprintf; use function strlen; -use const CURLE_OK; -use const CURLINFO_RESPONSE_CODE; -use const CURLOPT_HTTPHEADER; -use const CURLOPT_POST; -use const CURLOPT_POSTFIELDS; -use const CURLOPT_RETURNTRANSFER; -use const CURLOPT_URL; -use const JSON_THROW_ON_ERROR; - /** * SDK Prototype. This makes no attempt to short-circuit the network for * internal use, forcing a completely dogfooded experience. @@ -45,9 +27,12 @@ class Client private string $secretKey; + private Transport\TransportInterface $transport; + public function __construct( #[SensitiveParameter] ?string $secretKey = null, private string $apiHost = self::DEFAULT_API_HOST, + ?Transport\TransportInterface $transport = null, ) { // Auto-detect if not provided if ($secretKey === null) { @@ -68,6 +53,7 @@ public function __construct( } $this->secretKey = $secretKey; + $this->transport = $transport ?? new Transport\Curl(); } public function verifyAuthToken(string $authToken): AuthResponse @@ -107,51 +93,13 @@ public function attachRegistration(string $regToken, array $user): Credential */ public function makeApiCall(string $route, array $params): array { - // TODO: PSR-xx - $json = json_encode($params, JSON_THROW_ON_ERROR); - $ch = curl_init(); - curl_setopt_array($ch, [ - CURLOPT_URL => sprintf('%s%s', $this->apiHost, $route), - CURLOPT_POST => 1, - CURLOPT_POSTFIELDS => $json, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => [ - 'Authorization: Basic ' . base64_encode(':' . $this->secretKey), - 'Accept: application/json', - 'Content-Type: application/json', - 'Content-Length: ' . strlen($json), - sprintf( - 'User-Agent: php-sdk/%s curl/%s php/%s', - InstalledVersions::getVersion('snapauth/sdk'), - curl_version()['version'] ?? 'unknown', - PHP_VERSION, - ), - sprintf('X-SDK: php/%s', InstalledVersions::getVersion('snapauth/sdk')), - ], - ]); - - try { - $response = curl_exec($ch); - $errno = curl_errno($ch); - $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); - - if ($response === false || $errno !== CURLE_OK) { - $this->error(); - } - - if ($code >= 300) { - $this->error(); - } - // Handle non-200s, non-JSON (severe upstream error) - assert(is_string($response)); - $decoded = json_decode($response, true, flags: JSON_THROW_ON_ERROR); - assert(is_array($decoded)); - return $decoded['result']; - } catch (JsonException) { + $url = sprintf('%s%s', $this->apiHost, $route); + $result = $this->transport->makeApiCall(url: $url, params: $params); + if ($result->code >= 300) { $this->error(); - } finally { - curl_close($ch); } + $decoded = $result->decoded; + return $decoded['result']; } /** @@ -168,6 +116,7 @@ public function __debugInfo(): array return [ 'apiHost' => $this->apiHost, 'secretKey' => substr($this->secretKey, 0, 9) . '***' . substr($this->secretKey, -2), + 'transport' => get_class($this->transport), ]; } } diff --git a/src/Transport/Curl.php b/src/Transport/Curl.php new file mode 100644 index 0000000..4248875 --- /dev/null +++ b/src/Transport/Curl.php @@ -0,0 +1,74 @@ + $url, + CURLOPT_POST => 1, + CURLOPT_POSTFIELDS => $json, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ + 'Authorization: Basic ' . base64_encode(':' . $this->secretKey), + 'Accept: application/json', + 'Content-Type: application/json', + 'Content-Length: ' . strlen($json), + sprintf( + 'User-Agent: php-sdk/%s curl/%s php/%s', + InstalledVersions::getVersion('snapauth/sdk'), + curl_version()['version'] ?? 'unknown', + PHP_VERSION, + ), + sprintf('X-SDK: php/%s', InstalledVersions::getVersion('snapauth/sdk')), + ], + ]); + + try { + $response = curl_exec($ch); + $errno = curl_errno($ch); + $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + + if ($response === false || $errno !== CURLE_OK) { + $this->error(); + } + + // Handle non-200s, non-JSON (severe upstream error) + assert(is_string($response)); + $decoded = json_decode($response, true, flags: JSON_THROW_ON_ERROR); + assert(is_array($decoded)); + return new Response(code: $code, decoded: $decoded); + } catch (JsonException) { + $this->error(); + } finally { + curl_close($ch); + } + } +} diff --git a/src/Transport/Psr.php b/src/Transport/Psr.php new file mode 100644 index 0000000..b2cee06 --- /dev/null +++ b/src/Transport/Psr.php @@ -0,0 +1,46 @@ +streamFactory->createStream($json); + + $request = $this->requestFactory + ->createRequest(method: 'POST', uri: $url) + ->withHeader('Authoriation', 'Basic blahblah') // FIXME + ->withHeader('Accept', 'application/json') + ->withHeader('Content-type', 'application/json') + ->withHeader('Content-length', (string) strlen($json)) + ->withHeader('User-agent', 'blahblah psr') // FIXME + ->withHeader('X-SDK', 'php/%s'); // FIXME + + // try/catch + $response = $this->client->sendRequest($request); + + $code = $response->getStatusCode(); + + $responseJson = (string) $response->getBody(); + if (!json_validate($responseJson)) { + // ?? + } + $decoded = json_decode($responseJson, true, flags: JSON_THROW_ON_ERROR); + assert(is_array($decoded)); + return new Response($code, $decoded); + } +} diff --git a/src/Transport/Response.php b/src/Transport/Response.php new file mode 100644 index 0000000..a07d2fe --- /dev/null +++ b/src/Transport/Response.php @@ -0,0 +1,21 @@ +