Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
71 changes: 10 additions & 61 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,16 @@

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;
use function json_encode;
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.
Expand All @@ -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) {
Expand All @@ -68,6 +53,7 @@ public function __construct(
}

$this->secretKey = $secretKey;
$this->transport = $transport ?? new Transport\Curl();
}

public function verifyAuthToken(string $authToken): AuthResponse
Expand Down Expand Up @@ -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'];
}

/**
Expand All @@ -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),
];
}
}
74 changes: 74 additions & 0 deletions src/Transport/Curl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

namespace SnapAuth\Transport;

use Composer\InstalledVersions;
use JsonException;

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 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;

final class Curl implements TransportInterface
{
public function makeApiCall(string $url, array $params): Response
{
// TODO: PSR-xx
$json = json_encode($params, JSON_THROW_ON_ERROR);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $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);
}
}
}
46 changes: 46 additions & 0 deletions src/Transport/Psr.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace SnapAuth\Transport;

use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\{RequestFactoryInterface, StreamFactoryInterface};

final class Psr implements TransportInterface
{
public function __construct(
public readonly ClientInterface $client,
public readonly RequestFactoryInterface $requestFactory,
public readonly StreamFactoryInterface $streamFactory,
) {
}

public function makeApiCall(string $url, array $params): Response
{
$json = json_encode($params, JSON_THROW_ON_ERROR);
$stream = $this->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);
}
}
21 changes: 21 additions & 0 deletions src/Transport/Response.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace SnapAuth\Transport;

/**
* This is a vastly simplified version of PSR-7 ResponseInterface. It's used to
* avoid forcing an external dependency on clients.
*/
final class Response
{
/**
* @param mixed[] $decoded
*/
public function __construct(
public readonly int $code,
public readonly array $decoded,
) {
}
}
17 changes: 17 additions & 0 deletions src/Transport/TransportInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace SnapAuth\Transport;

interface TransportInterface
{
/**
* @internal This method is made public for cases where APIs do not have
* native SDK support, but is NOT considered part of the public, stable
* API and is not subject to SemVer.
*
* @param mixed[] $params
*/
public function makeApiCall(string $url, array $params): Response;
}