connectors

object

%Connector

/phlo/resources/connectors/Connector.phlo
version 1.0
creator q-ai.nl
summary Base class for API connectors: credentials, JSON requests, retries, pagination and a normalized result contract
package connectors
frontend false
backend true
requires creds HTTP
tags api connector http rest base
const

Connector :: section

line 10
void
const

Connector :: api

line 11
void
method

%Connector -> __construct (?array $config = null)

line 13
if ($config === null){
	$section = static::section
	$creds = $section ? %creds->{$section} : null
	$config = $creds ? (array)$creds->toArray : []
}
$this->config = $config
$this->timeout = 15
$this->retries = 0
static

Connector :: make (?array $config = null):static

line 24
new static($config)
method

%Connector -> base

line 26
static::api
method

%Connector -> headers

line 27
[]
static

Connector :: fields

line 29
[]
method

%Connector -> configured (...$keys):bool

line 31
foreach ($keys AS $key){
	if (($this->config[$key] ?? void) === void) return false
}
return true
method

%Connector -> missing (...$keys):?obj

line 38
return $this->configured(...$keys) ? null : static::fail(static::section.' credentials not configured ('.implode(', ', $keys).')')
static

Connector :: bearer ($token):string

line 42
'Authorization: Bearer '.$token
static

Connector :: basic ($user, $pass):string

line 43
'Authorization: Basic '.base64_encode($user.colon.$pass)
static

Connector :: build (string $method, string $url, ?array $query = null, array $headers = [], mixed $json = null, mixed $form = null):array

line 45
if ($query) $url .= (str_contains($url, qm) ? '&' : qm).http_build_query($query)
$body = null
if ($json !== null){
	$body = is_string($json) ? $json : json_encode($json, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
	$headers[] = 'Content-Type: application/json'
}
elseif ($form !== null){
	$body = is_string($form) ? $form : http_build_query($form)
	$headers[] = 'Content-Type: application/x-www-form-urlencoded'
}
$headers[] = 'Accept: application/json'
return ['method' => strtoupper($method), 'url' => $url, 'headers' => $headers, 'body' => $body]
static

Connector :: ok ($data, int $status = 200):obj

line 60
obj(ok: true, status: $status, data: $data)
static

Connector :: fail ($error, int $status = 0):obj

line 61
obj(ok: false, status: $status, error: $error)
static

Connector :: errorMessage ($data, string $raw, int $status):string

line 63
if (is_object($data)){
	if (isset($data->error->message)) return (string)$data->error->message
	if (isset($data->error) && is_string($data->error)) return $data->error
	if (isset($data->message)) return (string)$data->message
	if (isset($data->errors)){
		$errors = $data->errors
		if (is_string($errors)) return $errors
		if (is_array($errors)) return is_string($errors[0] ?? null) ? $errors[0] : json_encode($errors)
		if (is_object($errors)) return json_encode($errors)
	}
}
return $raw !== void ? $raw : 'HTTP '.$status
static

Connector :: parse ($raw, int $status = 200):obj

line 78
$raw = (string)$raw
$data = $raw === void ? null : json_decode($raw)
if ($status < 200 || $status >= 300) return static::fail(static::errorMessage($data, $raw, $status), $status)
return obj(ok: true, status: $status, data: $data ?? $raw)
static

Connector :: retryable ($method, int $status):bool

line 85
in_array($method, ['GET', 'HEAD']) && ($status === 429 || $status >= 500)
static

Connector :: backoff (int $attempt, $response):int

line 87
$after = (int)($response->headers['retry-after'] ?? 0)
return $after > 0 ? min($after, 30) * 1000000 : 200000 * $attempt
method

%Connector -> dispatch (array $req):obj

line 92
$method = $req['method']
$body = $req['body']
$attempt = 0
$response = null
while (true){
	try {
		if ($method === 'GET') $raw = HTTP($req['url'], $req['headers'], cookies: false, timeout: $this->timeout, response: $response)
		elseif ($method === 'DELETE') $raw = HTTP($req['url'], $req['headers'], DELETE: true, cookies: false, timeout: $this->timeout, response: $response)
		elseif ($method === 'PUT') $raw = HTTP($req['url'], $req['headers'], PUT: $body ?? void, cookies: false, timeout: $this->timeout, response: $response)
		elseif ($method === 'PATCH') $raw = HTTP($req['url'], $req['headers'], PATCH: $body ?? void, cookies: false, timeout: $this->timeout, response: $response)
		else $raw = HTTP($req['url'], $req['headers'], POST: $body ?? void, cookies: false, timeout: $this->timeout, response: $response)
	}
	catch (\Throwable $e){
		return static::fail($e->getMessage(), 0)
	}
	$status = $response->status ?? 0
	if (($status >= 200 && $status < 300) || !static::retryable($method, $status) || $attempt >= $this->retries) break
	usleep(static::backoff(++$attempt, $response))
}
$result = static::parse($raw, $status)
$result->headers = $response->headers ?? []
return $result
method

%Connector -> request (string $method, string $url, ?array $query = null, array $headers = [], mixed $json = null, mixed $form = null):obj

line 117
if (!str_starts_with($url, 'http')) $url = rtrim((string)$this->base, slash).slash.ltrim($url, slash)
$headers = array_merge((array)$this->headers, $headers)
return $this->dispatch(static::build($method, $url, $query, $headers, $json, $form))
method

%Connector -> get (string $url, ?array $query = null, array $headers = []):obj

line 123
$this->request('GET', $url, query: $query, headers: $headers)
method

%Connector -> post (string $url, mixed $json = null, array $headers = []):obj

line 124
$this->request('POST', $url, headers: $headers, json: $json)
method

%Connector -> put (string $url, mixed $json = null, array $headers = []):obj

line 125
$this->request('PUT', $url, headers: $headers, json: $json)
method

%Connector -> patch (string $url, mixed $json = null, array $headers = []):obj

line 126
$this->request('PATCH', $url, headers: $headers, json: $json)
method

%Connector -> del (string $url, array $headers = []):obj

line 127
$this->request('DELETE', $url, headers: $headers)
method

%Connector -> form (string $url, array $fields, array $headers = []):obj

line 128
$this->request('POST', $url, headers: $headers, form: $fields)
method

%Connector -> paginate (string $url, callable $extract, ?array $query = null, string $param = 'page', int $start = 1, int $max = 0):array

line 130
$items = []
$page = $start
while (true){
	$res = $this->get($url, ($query ?? []) + [$param => $page])
	if (!$res->ok) break
	$batch = $extract($res->data)
	if (!$batch) break
	foreach ($batch AS $item) $items[] = $item
	if ($max && count($items) >= $max) break
	$page++
}
return $items
object

%OAuthConnector

/phlo/resources/connectors/OAuthConnector.phlo
version 1.0
creator q-ai.nl
summary Base class for OAuth2 connectors: stored, auto-refreshed bearer access tokens
extends Connector
package connectors
frontend false
backend true
requires @Connector TokenStore
tags oauth oauth2 connector base token refresh
const

OAuthConnector :: tokenUrl

line 11
void
method

%OAuthConnector -> oauthKey

line 13
static::section
prop

%OAuthConnector -> token

line 15
TokenStore::access($this->oauthKey, static::tokenUrl, $this->config['client_id'] ?? void, $this->config['client_secret'] ?? void, ['refresh_token' => $this->config['refresh_token'] ?? null])
method

%OAuthConnector -> headers

line 17
[static::bearer((string)$this->token)]
method

%OAuthConnector -> authed

line 19
(string)$this->token !== void
object

%TokenStore

/phlo/resources/connectors/TokenStore.phlo
version 1.0
creator q-ai.nl
summary Persisted OAuth2 token store with automatic refresh via the OAuth2 resource
package connectors
frontend false
backend true
requires OAuth2
tags oauth oauth2 token refresh store credentials
static

TokenStore :: path ($key):string

line 10
data.'tokens/'.preg_replace('/[^a-z0-9_.-]+/i', us, (string)$key).'.json'
static

TokenStore :: read ($key):array

line 12
$file = static::path($key)
if (!is_file($file)) return []
@chmod(data.'tokens', 0700)
@chmod($file, 0600)
return (array)json_read($file, true)
static

TokenStore :: write ($key, array $token):void

line 20
$dir = data.'tokens'
is_dir($dir) || mkdir($dir, 0700, true)
@chmod($dir, 0700)
$file = static::path($key)
json_write($file, $token)
@chmod($file, 0600)
static

TokenStore :: valid (array $token):bool

line 29
($token['access_token'] ?? void) !== void && (int)($token['expires_at'] ?? 0) > time() + 30
static

TokenStore :: store ($res, $refresh):array

line 31
$token = [
	'access_token' => $res['access_token'],
	'refresh_token' => $res['refresh_token'] ?? $refresh,
	'expires_at' => time() + (int)($res['expires_in'] ?? 3600),
]
return $token
static

TokenStore :: lock ($key)

line 42
$dir = data.'tokens'
is_dir($dir) || mkdir($dir, 0700, true)
$lock = @fopen(static::path($key).'.lock', 'c')
if (!$lock || !flock($lock, LOCK_EX)){
	$lock && fclose($lock)
	return null
}
return $lock
static

TokenStore :: access ($key, $tokenUrl, $clientId, $clientSecret, array $seed = []):?string

line 53
$token = static::read($key)
if (static::valid($token)) return $token['access_token']
$lock = static::lock($key)
if (!$lock) return null
try {
	$token = static::read($key)
	if (!($token['refresh_token'] ?? null) && ($seed['refresh_token'] ?? null)){
		$token = ['refresh_token' => $seed['refresh_token']]
		static::write($key, $token)
	}
	if (static::valid($token)) return $token['access_token']
	$refresh = $token['refresh_token'] ?? null
	if (!$refresh || !$tokenUrl || !$clientId) return null
	$res = OAuth2::refresh($tokenUrl, $clientId, $clientSecret, $refresh)
	if (!($res['access_token'] ?? null)) return null
	$token = static::store($res, $refresh)
	static::write($key, $token)
	return $token['access_token']
} finally {
	flock($lock, LOCK_UN)
	fclose($lock)
}

We gebruiken essentiële cookies om deze site te laten werken. Met uw toestemming gebruiken we ook analytics om de site te verbeteren.