Compare commits

...

16 Commits

Author SHA1 Message Date
Clemens Schwaighofer
5c8a2ef8da Update test paths for URLRequests tests 2024-11-06 10:38:30 +09:00
Clemens Schwaighofer
d8379a10d9 URL Request phpunit test added 2024-11-06 10:33:05 +09:00
Clemens Schwaighofer
30e2f33620 Test calls update for admin area 2024-11-06 10:03:33 +09:00
Clemens Schwaighofer
a4f16f4ca9 Various updates and fixes during testing
Move the build auth content to dedicated variables
Add a default User-Agent that is always sent
Default headers like Authorization and User-Agent are always set, even when
request is sent with headers null
Fix timeout, was sent as is and not converted to milliseconds
Fix headers not correctly set to null if array entry was set to null
2024-11-06 10:03:14 +09:00
Clemens Schwaighofer
6e7b9cd033 phpunit URL Requests backend test file 2024-11-01 14:43:10 +09:00
Clemens Schwaighofer
4bc2ad8fa0 URL Requests basic tests file 2024-11-01 14:42:43 +09:00
Clemens Schwaighofer
0d4e959f39 Remove the nice formatter for now 2024-11-01 14:42:04 +09:00
Clemens Schwaighofer
95d567545a URL Requests via curl, a simple library 2024-11-01 14:41:46 +09:00
Clemens Schwaighofer
d89c6d1bde UrlRequests target file renamed 2024-10-29 18:28:19 +09:00
Clemens Schwaighofer
337ebb9032 Add a localhost entry to the hosts config 2024-10-29 18:28:07 +09:00
Clemens Schwaighofer
9538ebce7b Merge branch 'NewFeatures' into Feature-UrlRequestsCurl 2024-10-29 14:14:10 +09:00
Clemens Schwaighofer
1bff19f4b6 Update UrlRequests with patch, admin test page for it
Also update delete to have optional body (content)
2024-10-28 17:05:49 +09:00
Clemens Schwaighofer
f781b5e55f Name update for params/query
Order in methods
url: mandatory
payload: mandatory in post/put
header = []
query = ""

old "params" -> "payload"
2024-10-21 09:52:49 +09:00
Clemens Schwaighofer
934db50b3a Merge branch 'NewFeatures' into Feature-UrlRequestsCurl 2024-10-21 09:52:09 +09:00
Clemens Schwaighofer
3c5200cd99 Test run for Curl URL Requests 2024-10-21 09:32:20 +09:00
Clemens Schwaighofer
50a4b88f55 UrlRequests\Curl class
Basic interface class to CURL calls

Open:
clean up and check code is neutral
write tests, for this we need a running localhost server for tests to request to
2024-10-21 09:25:39 +09:00
10 changed files with 2702 additions and 8 deletions

View File

@@ -0,0 +1,48 @@
<?php // phpcs:ignore PSR1.Files.SideEffects
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: Ymd
* DESCRIPTION:
* DescriptionHere
*/
declare(strict_types=1);
/**
* build return json
*
* @param array $http_headers
* @param string $body
* @return string
*/
function buildContent(array $http_headers, string $body): string
{
return json_encode([
'HEADERS' => $http_headers,
"REQUEST_TYPE" => $_SERVER['REQUEST_METHOD'],
"PARAMS" => $_GET,
"BODY" => json_decode($body, true)
]);
}
$http_headers = array_filter($_SERVER, function ($value, $key) {
if (str_starts_with($key, 'HTTP_')) {
return true;
}
}, ARRAY_FILTER_USE_BOTH);
header("Content-Type: application/json; charset=UTF-8");
if (!empty($http_headers['HTTP_AUTHORIZATION']) && !empty($http_headers['HTTP_RUNAUTHTEST'])) {
header("HTTP/1.1 401 Unauthorized");
print buildContent($http_headers, '["code": 401, "content": {"Error" => "Not Authorized"}]');
exit;
}
print buildContent(
$http_headers,
file_get_contents('php://input') ?: '["code": 500, "content": {"Error" => "file_get_contents failed"}]'
);
// __END__

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
# PHP Stan Config
includes:
- phpstan-conditional.php
- ./vendor/yamadashy/phpstan-friendly-formatter/extension.neon
#- ./vendor/yamadashy/phpstan-friendly-formatter/extension.neon
parameters:
tmpDir: %currentWorkingDirectory%/tmp/phpstan-corelibs
errorFormat: friendly
friendly:
lineBefore: 5
lineAfter: 3
#errorFormat: friendly
#friendly:
# lineBefore: 3
# lineAfter: 3
level: 8 # max is now 9
# strictRules:
# allRules: true
@@ -60,6 +60,6 @@ parameters:
# paths:
# - ...
# - ...
-
message: "#^Call to deprecated method #"
path: www/admin/class_test*.php
# -
# message: "#^Call to deprecated method #"
# path: www/admin/class_test*.php

View File

@@ -0,0 +1,61 @@
<?php // phpcs:ignore PSR1.Files.SideEffects
declare(strict_types=1);
// url requests target test
require 'config.php';
use CoreLibs\Convert\Json;
$LOG_FILE_ID = 'classTest-urlrequests-target';
$log = new CoreLibs\Logging\Logging([
'log_folder' => BASE . LOG,
'log_file_id' => $LOG_FILE_ID,
'log_per_date' => true,
]);
/**
* build return json
*
* @param array<string,mixed> $http_headers
* @param string $body
* @return string
*/
function buildContent(array $http_headers, string $body): string
{
return Json::jsonConvertArrayTo([
'HEADERS' => $http_headers,
"REQUEST_TYPE" => $_SERVER['REQUEST_METHOD'],
"PARAMS" => $_GET,
"BODY" => Json::jsonConvertToArray($body)
]);
}
$http_headers = array_filter($_SERVER, function ($value, $key) {
if (str_starts_with($key, 'HTTP_')) {
return true;
}
}, ARRAY_FILTER_USE_BOTH);
header("Content-Type: application/json; charset=UTF-8");
// if the header has Authorization and RunAuthTest then exit with 401
if (!empty($http_headers['HTTP_AUTHORIZATION']) && !empty($http_headers['HTTP_RUNAUTHTEST'])) {
header("HTTP/1.1 401 Unauthorized");
print buildContent($http_headers, '["code": 401, "content": {"Error" => "Not Authorized"}]');
exit;
}
$file_get = file_get_contents('php://input') ?: '{"Error" => "file_get_contents failed"}';
// str_replace('\"', '"', trim($file_get, '"'));
$log->debug('SERVER', $log->prAr($_SERVER));
$log->debug('HEADERS', $log->prAr($http_headers));
$log->debug('REQUEST TYPE', $_SERVER['REQUEST_METHOD']);
$log->debug('GET', $log->prAr($_GET));
$log->debug('POST', $log->prAr($_POST));
$log->debug('PHP-INPUT', $log->prAr($file_get));
print buildContent($http_headers, $file_get);
$log->debug('[END]', '=========================================>');
// __END__

View File

@@ -117,6 +117,7 @@ $test_files = [
'class_test.config.direct.php' => 'Class Test: CONFIG DIRECT',
'class_test.class-calls.php' => 'Class Test: CLASS CALLS',
'class_test.error_msg.php' => 'Class Test: ERROR MSG',
'class_test.url-requests.curl.php' => 'Class Test: URL REQUESTS: CURL',
'subfolder/class_test.config.direct.php' => 'Class Test: CONFIG DIRECT SUB',
];

View File

@@ -0,0 +1,295 @@
<?php // phpcs:ignore warning
/**
* @phan-file-suppress PhanTypeSuspiciousStringExpression
*/
declare(strict_types=1);
error_reporting(E_ALL | E_STRICT | E_ERROR | E_WARNING | E_PARSE | E_COMPILE_ERROR);
ob_start();
// basic class test file
define('USE_DATABASE', false);
// sample config
require 'config.php';
// define log file id
$LOG_FILE_ID = 'classTest-urlrequests';
ob_end_flush();
use CoreLibs\UrlRequests\Curl;
$log = new CoreLibs\Logging\Logging([
'log_folder' => BASE . LOG,
'log_file_id' => $LOG_FILE_ID,
'log_per_date' => true,
]);
$PAGE_NAME = 'TEST CLASS: URL REQUESTS CURL';
print "<!DOCTYPE html>";
print "<html><head><title>" . $PAGE_NAME . "</title></head>";
print "<body>";
print '<div><a href="class_test.php">Class Test Master</a></div>';
print '<div><h1>' . $PAGE_NAME . '</h1></div>';
$client = new Curl();
print "<hr>";
$data = $client->get(
'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php'
. '?other=get_a',
[
'headers' => $client->prepareHeaders([
'test-header: ABC',
'info-request-type: _GET',
'Funk-pop' => 'Semlly god'
]),
'query' => ['foo' => 'BAR']
]
);
print "_GET RESPONSE: <pre>" . print_r($data, true) . "</pre>";
print "<hr>";
$data = $client->request(
'get',
'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php'
. '?other=get_a',
);
print "_GET RESPONSE, nothing set: <pre>" . print_r($data, true) . "</pre>";
print "<hr>";
try {
$data = $client->request(
'get',
'soba54.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php'
. '?other=get_a',
);
print "_GET RESPONSE, nothing set, invalid URL: <pre>" . print_r($data, true) . "</pre>";
} catch (Exception $e) {
print "Exception: <pre>" . print_r($e, true) . "</pre><br>";
}
print "<hr>";
$data = $client->request(
"get",
'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/'
. 'trunk/www/admin/UrlRequests.target.php'
. '?other=get_a',
[
"headers" => $client->prepareHeaders([
'test-header: ABC',
'info-request-type: _GET',
'Funk-pop' => 'Semlly god'
]),
"query" => ['foo' => 'BAR'],
],
);
print "[request] _GET RESPONSE: <pre>" . print_r($data, true) . "</pre>";
print "<hr>";
$data = $client->post(
'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php'
. '?other=post_a',
[
'body' => ['payload' => 'data post'],
'headers' => $client->prepareHeaders([
'Content-Type: application/json',
'Accept: application/json',
'test-header: ABC',
'info-request-type: _POST'
]),
'query' => ['foo' => 'BAR post'],
]
);
print "_POST RESPONSE: <pre>" . print_r($data, true) . "</pre>";
print "<hr>";
$data = $client->request(
"post",
'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php'
. '?other=post_a',
[
"body" => ['payload' => 'data post', 'request' => 'I am the request body'],
"headers" => $client->prepareHeaders([
'Content-Type: application/json',
'Accept: application/json',
'test-header: ABC',
'info-request-type: _POST'
]),
"query" => ['foo' => 'BAR post'],
]
);
print "[request] _POST RESPONSE: <pre>" . print_r($data, true) . "</pre>";
print "<hr>";
$data = $client->put(
'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php'
. '?other=put_a',
[
"body" => ['payload' => 'data put'],
"headers" => $client->prepareHeaders([
'Content-Type: application/json',
'Accept: application/json',
'test-header: ABC',
'info-request-type: _PUT'
]),
'query' => ['foo' => 'BAR put'],
]
);
print "_PUT RESPONSE: <pre>" . print_r($data, true) . "</pre>";
print "<hr>";
$data = $client->patch(
'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php'
. '?other=patch_a',
[
"body" => ['payload' => 'data patch'],
"headers" => $client->prepareHeaders([
'Content-Type: application/json',
'Accept: application/json',
'test-header: ABC',
'info-request-type: _PATCH'
]),
'query' => ['foo' => 'BAR patch'],
]
);
print "_PATCH RESPONSE: <pre>" . print_r($data, true) . "</pre>";
print "<hr>";
$data = $client->delete(
'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php'
. '?other=delete_no_body_a',
[
"body" => null,
"headers" => $client->prepareHeaders([
'Content-Type: application/json',
'Accept: application/json',
'test-header: ABC',
'info-request-type: _DELETE'
]),
"query" => ['foo' => 'BAR delete'],
]
);
print "_DELETE RESPONSE: <pre>" . print_r($data, true) . "</pre>";
print "<hr>";
$data = $client->delete(
'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/UrlRequests.target.php'
. '?other=delete_body_a',
[
"body" => ['payload' => 'data delete'],
"headers" => $client->prepareHeaders([
'Content-Type: application/json',
'Accept: application/json',
'test-header: ABC',
'info-request-type: _DELETE'
]),
"query" => ['foo' => 'BAR delete'],
]
);
print "_DELETE RESPONSE BODY: <pre>" . print_r($data, true) . "</pre>";
print "<hr>";
try {
$uc = new Curl([
"base_uri" => 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/foo',
"headers" => [
'DEFAULT-master' => 'master-header',
'default-header' => 'uc-get',
'default-remove' => 'will be removed',
'default-remove-array' => ['a', 'b'],
'default-remove-array-part' => ['c', 'd'],
'default-remove-array-part-alt' => ['c', 'd', 'e'],
'default-overwrite' => 'will be overwritten',
'default-add' => 'will be added',
],
'query' => [
'global-p' => 'glob'
]
]);
print "CONFIG: <pre>" . print_r($uc->getConfig(), true) . "</pre>";
$uc->removeHeaders(['default-remove' => '']);
$uc->removeHeaders(['default-remove-array' => ['a', 'b']]);
$uc->removeHeaders(['default-remove-array-part' => 'c']);
$uc->removeHeaders(['default-remove-array-part-alt' => ['c', 'd']]);
$uc->setHeaders(['default-new' => 'Something new']);
$uc->setHeaders(['default-overwrite' => 'Something Overwritten']);
$uc->setHeaders(['default-add' => 'Something Added'], true);
print "CONFIG: <pre>" . print_r($uc->getConfig(), true) . "</pre>";
$data = $uc->request(
'get',
'UrlRequests.target.php',
[
'headers' => [
'call-header' => 'call-get',
'default-header' => 'overwrite-uc-get',
'X-Foo' => ['bar', 'baz'],
],
'query' => [
'other' => 'get_a',
],
]
);
print "[uc] _GET RESPONSE, nothing set: <pre>" . print_r($data, true) . "</pre>";
print "[uc] SENT URL: " . $uc->getUrlSent() . "<br>";
print "[uc] SENT URL PARSED: <pre>" . print_r($uc->getUrlParsedSent(), true) . "</pre>";
print "[uc] SENT HEADERS: <pre>" . print_r($uc->getHeadersSent(), true) . "</pre>";
} catch (Exception $e) {
print "Exception: <pre>" . print_r(json_decode($e->getMessage(), true), true) . "</pre><br>";
}
print "<hr>";
try {
$uc = new Curl([
"base_uri" => 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/',
"exception_on_not_authorized" => false,
"headers" => [
"Authorization" => "schmalztiegel",
"RunAuthTest" => "yes",
]
]);
$response = $uc->get('UrlRequests.target.php');
print "AUTH REQUEST: <pre>" . print_r($response, true) . "</pre>";
print "[uc] SENT URL: " . $uc->getUrlSent() . "<br>";
print "[uc] SENT URL PARSED: <pre>" . print_r($uc->getUrlParsedSent(), true) . "</pre>";
print "[uc] SENT HEADERS: <pre>" . print_r($uc->getHeadersSent(), true) . "</pre>";
} catch (Exception $e) {
print "Exception: <pre>" . print_r(json_decode($e->getMessage(), true), true) . "</pre><br>";
}
print "AUTH REQUEST WITH EXCEPTION:<br>";
try {
$uc = new Curl([
"base_uri" => 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/',
"exception_on_not_authorized" => true,
"headers" => [
"Authorization" => "schmalztiegel",
"RunAuthTest" => "yes",
]
]);
$response = $uc->get('UrlRequests.target.php');
print "AUTH REQUEST: <pre>" . print_r($response, true) . "</pre>";
print "[uc] SENT URL: " . $uc->getUrlSent() . "<br>";
print "[uc] SENT URL PARSED: <pre>" . print_r($uc->getUrlParsedSent(), true) . "</pre>";
print "[uc] SENT HEADERS: <pre>" . print_r($uc->getHeadersSent(), true) . "</pre>";
} catch (Exception $e) {
print "Exception: <pre>" . print_r(json_decode($e->getMessage(), true), true) . "</pre><br>";
}
print "<hr>";
$uc = new Curl([
"base_uri" => 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/',
"headers" => [
"header-one" => "one"
]
]);
$response = $uc->get('UrlRequests.target.php', ["headers" => null, "query" => ["test" => "one-test"]]);
print "AUTH REQUEST: <pre>" . print_r($response, true) . "</pre>";
print "[uc] SENT URL: " . $uc->getUrlSent() . "<br>";
print "[uc] SENT URL PARSED: <pre>" . print_r($uc->getUrlParsedSent(), true) . "</pre>";
print "[uc] SENT HEADERS: <pre>" . print_r($uc->getHeadersSent(), true) . "</pre>";
print "</body></html>";
// __END__

View File

@@ -51,6 +51,7 @@ $SITE_CONFIG = [
'soba.tequila.jp' => $__LOCAL_CONFIG,
'soba.teq.jp' => $__LOCAL_CONFIG,
'soba-local.tokyo.tequila.jp' => $__LOCAL_CONFIG,
'localhost' => $__LOCAL_CONFIG,
];
// __END__

View File

@@ -0,0 +1,970 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/9/20
* DESCRIPTION:
* Curl Client for get/post/put/delete requests through the php curl inteface
*
* For anything more complex use guzzlehttp/http
* https://docs.guzzlephp.org/en/stable/index.html
*
* Requests are guzzleHttp compatible
* Config for setup is guzzleHttp compatible (except the exception_on_not_authorized)
* Any setters and getters are only for this class
*/
declare(strict_types=1);
namespace CoreLibs\UrlRequests;
use RuntimeException;
use CoreLibs\Convert\Json;
/** @package CoreLibs\UrlRequests */
class Curl implements Interface\RequestsInterface
{
// all general calls: get/post/put/patch/delete
use CurlTrait;
/** @var array<string> all the valid request type */
private const VALID_REQUEST_TYPES = ["get", "post", "put", "patch", "delete"];
/** @var array<string> list of requests type that are set as custom in the curl options */
private const CUSTOM_REQUESTS = ["put", "patch", "delete"];
/** @var array<string> list of requests types that have _POST type fields */
private const HAVE_POST_FIELDS = ["post", "put", "patch", "delete"];
/** @var array<string> list of requests that must have a body */
private const MANDATORY_POST_FIELDS = ["post", "put", "patch"];
/** @var int error bad request */
public const HTTP_BAD_REQUEST = 400;
/** @var int error not authorized Request */
public const HTTP_NOT_AUTHORIZED = 401;
/** @var int error forbidden */
public const HTTP_FORBIDDEN = 403;
/** @var int error not found */
public const HTTP_NOT_FOUND = 404;
/** @var int error conflict */
public const HTTP_CONFLICT = 409;
/** @var int error unprocessable entity */
public const HTTP_UNPROCESSABLE_ENTITY = 422;
/** @var int http ok request */
public const HTTP_OK = 200;
/** @var int http ok creted response */
public const HTTP_CREATED = 201;
/** @var int http ok no content */
public const HTTP_NO_CONTENT = 204;
/** @var int major version for user agent */
public const MAJOR_VERSION = 1;
// the config is set to be as much compatible to guzzelHttp as possible
// phpcs:disable Generic.Files.LineLength
/** @var array{auth?:array{0:string,1:string,2:string},exception_on_not_authorized:bool,base_uri:string,headers:array<string,string|array<string>>,query:array<string,string>,timeout:float,connection_timeout:float} config settings as
*phpcs:enable Generic.Files.LineLength
* auth: [0: user, 1: password, 2: auth type]
* base_uri: base url to set, will prefix all urls given in calls
* headers: (array) base headers, can be overwritten by headers set in call
* timeout: default 0, in seconds (CURLOPT_TIMEOUT_MS)
* connect_timeout: default 300, in seconds (CURLOPT_CONNECTTIMEOUT_MS)
* : below is not a guzzleHttp config
* exception_on_not_authorized: bool true/false for throwing exception on auth error
*/
private array $config = [
'exception_on_not_authorized' => false,
'base_uri' => '',
'query' => [],
'headers' => [],
'timeout' => 0,
'connection_timeout' => 300,
];
/** @var array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string} parsed base_uri */
private array $parsed_base_uri = [];
/** @var array<string,string> lower key header name matches to given header name */
private array $headers_named = [];
/** @var int auth type from auth array in config */
private int $auth_type = 0;
/** @var string username and password string from auth array in config */
private string $auth_userpwd = '';
/** @var string set if auth type basic is given, will be set as "Authorization: ..." */
private string $auth_basic_header = '';
/** @var array<string,array<string>> received headers per header name, with sub array if there are redirects */
private array $received_headers = [];
/** @var string the current url sent */
private string $url = '';
/** @var array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string} parsed url to sent */
private array $parsed_url = [];
/** @var array<string,string> the current headers sent */
private array $headers = [];
/**
* see config allowe entries above
*
* @param array<string,mixed> $config config settings to be set
*/
public function __construct(array $config = [])
{
$this->setConfiguration($config);
}
// *********************************************************************
// MARK: PRIVATE METHODS
// *********************************************************************
/**
* Set the main configuration
*
* phpcs:disable Generic.Files.LineLength
* @param array{auth?:array{0:string,1:string,2:string},exception_on_not_authorized?:bool,base_uri?:string,headers?:array<string,string|array<string>>,query?:array<string,string>,timeout?:float,connection_timeout?:float} $config
* @return void
* phpcs:enable Generic.Files.LineLength
*/
private function setConfiguration(array $config)
{
$default_config = [
'exception_on_not_authorized' => false,
'base_uri' => '',
'query' => [],
'headers' => [],
'timeout' => 0,
'connection_timeout' => 300,
];
// auth string is array of 0: user, 1: password, 2: auth type
if (!empty($config['auth']) && is_array($config['auth'])) {
// base auth sets the header actually
$type = isset($config['auth'][2]) ? strtolower($config['auth'][2]) : 'basic';
$userpwd = $config['auth'][0] . ':' . $config['auth'][1];
switch ($type) {
case 'basic':
$this->auth_basic_header = 'Basic ' . base64_encode(
$userpwd
);
// if (!isset($config['headers']['Authorization'])) {
// $config['headers']['Authorization'] = 'Basic ' . base64_encode(
// $userpwd
// );
// }
break;
case 'digest':
$this->auth_type = CURLAUTH_DIGEST;
$this->auth_userpwd = $userpwd;
break;
case 'ntlm':
$this->auth_type = CURLAUTH_NTLM;
$this->auth_userpwd = $userpwd;
break;
}
}
// only set if bool
if (
!isset($config['exception_on_not_authorized']) ||
!is_bool($config['exception_on_not_authorized'])
) {
$config['exception_on_not_authorized'] = false;
}
if (!empty($config['base_uri'])) {
if (($parsed_base_uri = $this->parseUrl($config['base_uri'])) !== false) {
$this->parsed_base_uri = $parsed_base_uri;
$config['base_uri'] = $config['base_uri'];
}
}
// general headers
if (!empty($config['headers'])) {
// seat the key lookup with lower keys
foreach (array_keys($config['headers']) as $key) {
if (isset($this->headers_named[strtolower((string)$key)])) {
continue;
}
$this->headers_named[strtolower((string)$key)] = (string)$key;
}
}
// timeout (must be numeric)
if (!empty($config['timeout']) && !is_numeric($config['timeout'])) {
$config['timeout'] = 0;
}
if (!empty($config['connection_timeout']) && !is_numeric($config['connection_timeout'])) {
$config['connection_timeout'] = 300;
}
$this->config = array_merge($default_config, $config);
}
// MARK: parse and build url
/**
* From: https://github.com/guzzle/psr7/blob/a70f5c95fb43bc83f07c9c948baa0dc1829bf201/src/Uri.php#L106C5-L132C6
* guzzle/psr7::parse
*
* convert the url to valid sets
*
* @param string $url
* @return array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string}|false
*/
private function parseUrl(string $url): array|false
{
// If IPv6
$prefix = '';
if (preg_match('%^(.*://\[[0-9:a-f]+\])(.*?)$%', $url, $matches)) {
/** @var array{0:string, 1:string, 2:string} $matches */
$prefix = $matches[1];
$url = $matches[2];
}
/** @var string $encodedUrl */
$encodedUrl = preg_replace_callback(
'%[^:/@?&=#]+%usD',
static function ($matches) {
return urlencode($matches[0]);
},
$url
);
$result = parse_url($prefix . $encodedUrl);
if ($result === false) {
return false;
}
/** @var callable $caller */
$caller = 'urldecode';
return array_map($caller, $result);
}
/**
* build back the URL based on the parsed URL scheme
* NOTE: this is only a sub implementation
*
* phpcs:disable Generic.Files.LineLength
* @param array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string} $parsed_url
* @param bool $remove_until_slash [default=false]
* @param bool $add_query [default=false]
* @param bool $add_fragment [default=false]
* @return string
* phpcs:enable Generic.Files.LineLength
*/
private function buildUrl(
array $parsed_url,
bool $remove_until_slash = false,
bool $add_query = false,
bool $add_fragment = false
): string {
$url = '';
// scheme has :
if (!empty($parsed_url['scheme'])) {
$url .= $parsed_url['scheme'] . ':';
}
// host + port = authority
if (!empty($parsed_url['host'])) {
$url .= '//';
$url .= $parsed_url['host'] ?? '';
if (!empty($parsed_url['port'])) {
$url .= ':' . $parsed_url['port'];
}
}
// remove the last part "/.." because we do not end with "/"
if ($remove_until_slash) {
$url_path = $parsed_url['path'] ?? '';
if (($lastSlashPos = strrpos($url_path, '/')) !== false) {
$url .= substr($url_path, 0, $lastSlashPos + 1);
}
} else {
$url .= $parsed_url['path'] ?? '';
}
// only on demand
if ($add_query && !empty($parsed_url['query'])) {
$url .= '?' . $parsed_url['query'];
}
if ($add_fragment && !empty($parsed_url['fragment'])) {
$url .= '#' . $parsed_url['fragment'];
}
return $url;
}
// MARK: query, params and headers convert
/**
* Build URL with base url and parameters
*
* @param string $url_req to send
* @param null|array<string,string> $query any optional parameters to send
* @return string the fully build URL
*/
private function buildQuery(string $url_req, null|array $query = null): string
{
if (($parsed_url = $this->parseUrl($url_req)) !== false) {
$this->parsed_url = $parsed_url;
}
$url = $url_req;
if (
!empty($this->config['base_uri']) &&
empty($this->parsed_url['scheme'])
) {
if (str_ends_with($this->config['base_uri'], '/')) {
$url = $this->config['base_uri'] . $url_req;
} else {
// remove until last / and add url, strip leading / if set
// remove last "/" part until we are at the domain
// if we do not start with http(s):// then assume blank
// NOTE any fragments or params will get dropped, only path will remain
$url = $this->buildUrl($this->parsed_base_uri, remove_until_slash: true) . $url_req;
}
if (($parsed_url = $this->parseUrl($url)) !== false) {
$this->parsed_url = $parsed_url;
}
}
// build query with global query
// any query set in the base_url or url_req will be overwritten
if (!empty($this->config['query'])) {
// add current query if set
// for params: if foo[0] then we ADD as php array type
// note that this has to be done on the user side, we just merge and local overrides global
$query = array_merge($this->config['query'], $query ?? []);
}
if (is_array($query)) {
$query = http_build_query($query, '', '&', PHP_QUERY_RFC3986);
}
// add the params to the url
if (!empty($query)) {
// if the url_url has a query or a a fragment,
// we need to build that url new
// $parsed_url = false;
if (!empty($this->parsed_url['query']) || !empty($this->parsed_url['framgent'])) {
$url = $this->buildUrl($this->parsed_url);
}
$url .= '?' . $query;
// fragments are ignored
// if (!empty($this->parsed_url['fragment'])) {
// $url .= '#' . $parsed_url['fragment'];
// }
}
// parse again with current url
if ($url != $url_req) {
if (($parsed_url = $this->parseUrl($url)) !== false) {
$this->parsed_url = $parsed_url;
}
}
return $url;
}
/**
* Convert array body data to json type string
*
* @param string|array<string,mixed> $body
* @return string
*/
private function convertPayloadData(string|array $body): string
{
// convert to string as JSON block if it is an array
if (is_array($body)) {
$params = Json::jsonConvertArrayTo($body);
}
return $params ?? '';
}
/**
* header convert from array key -> value to string list
* if the key value is numeric, it is assumed this is an array string list only
* Note: this should not be the case
*
* @param array<string,string|array<string>> $headers
* @return array<string>
*/
private function convertHeaders(array $headers): array
{
$return_headers = [];
foreach ($headers as $key => $value) {
if (!is_string($key)) {
// TODO: throw error
continue;
}
// bad if not valid header key
if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $key)) {
// TODO throw error
continue;
}
// if value is array, join to string
if (is_array($value)) {
$value = join(', ', $value);
}
$value = trim((string)$value, " \t");
// header values must be valid
if (!preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) {
// TODO throw error
continue;
}
$return_headers[] = (string)$key . ':' . $value;
}
// remove empty entries
return $return_headers;
}
/**
* default headers that are always set
* Authorization
* User-Agent
*
* @return array<string,string>
*/
private function buildDefaultHeaders(): array
{
$headers = [];
// add auth header if set
if (!empty($this->auth_basic_header)) {
$headers['Authorization'] = $this->auth_basic_header;
}
// always add HTTP_HOST and HTTP_USER_AGENT
if (!isset($headers[strtolower('User-Agent')])) {
$headers['User-Agent'] = 'CoreLibsUrlRequestCurl/' . self::MAJOR_VERSION;
}
return $headers;
}
/**
* Build headers, combine with global headers of they are set
*
* @param null|array<string,string|array<string>> $headers
* @return array<string,string|array<string>>
*/
private function buildHeaders(null|array $headers): array
{
// if headers is null, return empty headers, do not set config default headers
// but the automatic set User-Agent and Authorization headers are always set
if ($headers === null) {
return $this->buildDefaultHeaders();
}
// merge master headers with sub headers, sub headers overwrite master headers
if (!empty($this->config['headers'])) {
// we need to build the current headers as a lookup table
$headers_lookup = [];
foreach (array_keys($headers) as $key) {
$headers_lookup[strtolower((string)$key)] = (string)$key;
}
// add config headers if not set in local header
foreach ($this->headers_named as $header_key => $key) {
// is set local, use this, else use global
if (isset($headers_lookup[$header_key])) {
continue;
}
$headers[$key] = $this->config['headers'][$key];
}
}
$headers = array_merge($headers, $this->buildDefaultHeaders());
return $headers;
}
// MARK: main curl request
/**
* Overall request call
*
* @param string $type get, post, pathc, put, delete:
* if not set or invalid throw error
* @param string $url The URL being requested,
* including domain and protocol
* @param null|array<string,string|array<string>> $headers [default=[]] Headers to be used in the request
* @param null|array<string,string> $query [default=null] Optinal query parameters
* @param null|string|array<string,mixed> $body [default=null] Data body, converted to JSON
* @return array{code:string,headers:array<string,array<string>>,content:string}
* @throws \RuntimeException if type param is not valid
*/
private function curlRequest(
string $type,
string $url,
null|array $headers = [],
null|array $query = null,
null|string|array $body = null
): array {
$this->url = $this->buildQuery($url, $query);
$this->headers = $this->convertHeaders($this->buildHeaders($headers));
if (!in_array($type, self::VALID_REQUEST_TYPES)) {
throw new RuntimeException(
json_encode([
'status' => 'FAILURE',
'code' => 'C003',
'type' => 'InvalidRequestType',
'message' => 'Invalid request type set: ' . $type,
'context' => [
'type' => $type,
'url' => $this->url,
'headers' => $this->headers,
],
]) ?: '',
0,
);
}
// init curl handle
$handle = $this->handleCurleInit($this->url);
// set the standard curl options
$this->setCurlOptions($handle, $this->headers);
// for post we set POST option
if ($type == "post") {
curl_setopt($handle, CURLOPT_POST, true);
} elseif (in_array($type, self::CUSTOM_REQUESTS)) {
curl_setopt($handle, CURLOPT_CUSTOMREQUEST, strtoupper($type));
}
// set body data if not null, will send empty [] for empty data
if (in_array($type, self::HAVE_POST_FIELDS) && $body !== null) {
curl_setopt($handle, CURLOPT_POSTFIELDS, $this->convertPayloadData($body));
}
// reset all headers before we start the call
$this->received_headers = [];
// run curl execute
$http_result = $this->handleCurlExec($handle);
// for debug
// print "CURLINFO_HEADER_OUT: <pre>" . curl_getinfo($handle, CURLINFO_HEADER_OUT) . "</pre>";
// get response code and bail on not authorized
$http_response = $this->handleCurlResponse($http_result, $handle);
// close handler
$this->handleCurlClose($handle);
// return response and result
return [
'code' => (string)$http_response,
'headers' => $this->received_headers,
'content' => (string)$http_result
];
}
// MARK: curl init
/**
* Handel curl init and errors
*
* @param string $url
* @return \CurlHandle
* @throws \RuntimeException if curl could not be initialized
*/
private function handleCurleInit(string $url): \CurlHandle
{
$handle = curl_init($url);
if ($handle !== false) {
return $handle;
}
// throw Error here with all codes
throw new RuntimeException(
json_encode([
'status' => 'FAILURE',
'code' => 'C001',
'type' => 'CurlInitError',
'message' => 'Failed to init curl with url: ' . $url,
'context' => [
'url' => $url,
],
]) ?: '',
0,
);
}
// MARK: set curl options and header collector
/**
* set the default curl options
*
* headers array: do not split into "key" => "value", they must be "key: value"
*
* @param \CurlHandle $handle
* @param array<string> $headers list of options
* @return void
*/
private function setCurlOptions(\CurlHandle $handle, array $headers): void
{
// for not Basic auth, basic auth sets its own header
if (!empty($this->auth_type) && !empty($this->auth_userpwd)) {
curl_setopt($handle, CURLOPT_HTTPAUTH, $this->auth_type);
curl_setopt($handle, CURLOPT_USERPWD, $this->auth_userpwd);
}
if ($headers !== []) {
curl_setopt($handle, CURLOPT_HTTPHEADER, $headers);
}
// curl_setopt($handle, CURLOPT_FAILONERROR, true);
// return response as string and not just HTTP_OK
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
// for debug only
curl_setopt($handle, CURLINFO_HEADER_OUT, true);
// curl_setopt($handle, CURLOPT_HEADER, true);
// collect the current request headers
curl_setopt($handle, CURLOPT_HEADERFUNCTION, [$this, 'collectCurlHttpHeaders']);
// if any timeout <1
$timeout_requires_no_signal = false;
// if we have a timeout signal
if (!empty($this->config['timeout'])) {
$timeout_requires_no_signal |= $this->config['timeout'] < 1;
curl_setopt($handle, CURLOPT_TIMEOUT_MS, $this->config['timeout'] * 1000);
}
if (!empty($this->config['connection_timeout'])) {
$timeout_requires_no_signal |= $this->config['connection_timeout'] < 1;
curl_setopt($handle, CURLOPT_CONNECTTIMEOUT_MS, $this->config['connection_timeout'] * 1000);
}
if ($timeout_requires_no_signal && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
curl_setopt($handle, CURLOPT_NOSIGNAL, true);
}
}
/**
* Collect HTTP headers
* They will be reset before each call
*
* @param \CurlHandle $curl current curl handle
* @param string $header header string to parse
* @return int size of current line of header
*/
private function collectCurlHttpHeaders(\CurlHandle $curl, string $header): int
{
$len = strlen($header);
$header = explode(':', $header, 2);
if (count($header) < 2) {
// ignore invalid headers
return $len;
}
$this->received_headers[strtolower(trim($header[0]))][] = trim($header[1]);
return $len;
}
// MARK: Curl Exception handler
/**
* handles any CURL execute and on error throws a correct error message
*
* @param \CurlHandle $handle Curl handler
* @return string Return content as string, if False will throw exception
* will only return HTTP_OK if CURLOPT_RETURNTRANSFER is turned off
* @throws \RuntimeException if the connection had an error
*/
private function handleCurlExec(\CurlHandle $handle): string
{
// execute query
$http_result = curl_exec($handle);
if ($http_result === true) {
// only if CURLOPT_RETURNTRANSFER
return (string)self::HTTP_OK;
} elseif ($http_result !== false) {
return $http_result;
}
$url = curl_getinfo($handle, CURLINFO_EFFECTIVE_URL);
$errno = curl_errno($handle);
$message = curl_error($handle);
switch ($errno) {
case CURLE_COULDNT_CONNECT:
case CURLE_COULDNT_RESOLVE_HOST:
case CURLE_OPERATION_TIMEOUTED:
$message = 'Could not connect to server (' . $url . '). Please check your '
. 'internet connection and try again. [' . $message . ']';
break;
case CURLE_SSL_PEER_CERTIFICATE:
$message = 'Could not verify SSL certificate. Please make sure '
. 'that your network is not intercepting certificates. '
. '(Try going to ' . $url . 'in your browser.) '
. '[' . $message . ']';
break;
case 0:
default:
$message = 'Unexpected error communicating with server: ' . $message;
}
// throw an error like in the normal reqeust, but set to CURL error
throw new RuntimeException(
json_encode([
'status' => 'FAILURE',
'code' => 'C002',
'type' => 'CurlError',
'message' => $message,
'context' => [
'url' => $url,
'errno' => $errno,
'message' => $message,
],
]) ?: '',
$errno
);
}
// MARK: curl response handler
/**
* Handle curl response and not auth 401 errors
*
* @param string $http_result
* @param \CurlHandle $handle
* @return string http response code
* @throws \RuntimeException Auth error
*/
private function handleCurlResponse(
string $http_result,
\CurlHandle $handle
): string {
$http_response = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
if (
empty($this->config['exception_on_not_authorized']) ||
$http_response !== self::HTTP_NOT_AUTHORIZED
) {
return (string)$http_response;
}
$err = curl_errno($handle);
// extract all the error codes
$result_ar = json_decode((string)$http_result, true);
$url = curl_getinfo($handle, CURLINFO_EFFECTIVE_URL);
// throw Error here with all codes
throw new RuntimeException(
json_encode([
'status' => 'ERROR',
'code' => $http_response,
'type' => 'UnauthorizedRequest',
'message' => 'Request could not be finished successfully because of an authorization error',
'context' => [
'url' => $url,
'result' => $result_ar,
],
]) ?: '',
$err
);
}
/**
* close the current curl handle
*
* @param \CurlHandle $handle
* @return void
*/
private function handleCurlClose(\CurlHandle $handle): void
{
curl_close($handle);
}
// *********************************************************************
// MARK: PUBLIC METHODS
// *********************************************************************
/**
* Convert an array with header strings like "foo: bar" to the interface
* needed "foo" => "bar" type
* Skips entries that are already in key => value type, by checking if the
* key is a not a number
*
* @param array<int|string,string> $headers
* @return array<string,string>
* @throws \UnexpectedValueException on duplicate header key
*/
public function prepareHeaders(array $headers): array
{
$return_headers = [];
foreach ($headers as $header_key => $header) {
// skip if header key is not numeric
if (!is_numeric($header_key)) {
$return_headers[$header_key] = $header;
continue;
}
list($_key, $_value) = explode(':', $header);
if (array_key_exists($_key, $return_headers)) {
// raise exception if key already exists
throw new \UnexpectedValueException(
json_encode([
'status' => 'ERROR',
'code' => 'C004',
'type' => 'DuplicatedArrayKey',
'message' => 'Key already exists in the headers',
'context' => [
'key' => $_key,
'headers' => $headers,
'return_headers' => $return_headers,
],
]) ?: '',
1
);
}
$return_headers[$_key] = $_value;
}
return $return_headers;
}
// MARK: get class vars
/**
* get the config array with all settings
*
* @return array<string,mixed> all current config settings
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Return the full url as it was sent
*
* @return string url sent
*/
public function getUrlSent(): string
{
return $this->url;
}
/**
* get the parsed url
*
* @return array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string}
*/
public function getUrlParsedSent(): array
{
return $this->parsed_url;
}
/**
* Return the full headers as they where sent
*
* @return array<string,string>
*/
public function getHeadersSent(): array
{
return $this->headers;
}
// MARK: set/remove for global headers
/**
* set, add or overwrite header
* On default this will overwrite header, and not set
*
* @param array<string,string|array<string>> $header
* @param bool $add [default=false] if set will add header to existing value
* @return void
*/
public function setHeaders(array $header, bool $add = false): void
{
foreach ($header as $key => $value) {
// check header previously set
if (isset($this->headers_named[strtolower($key)])) {
$header_key = $this->headers_named[strtolower($key)];
if ($add) {
// for this add we always add array on the right side
if (!is_array($value)) {
$value = (array)$value;
}
// if not array, rewrite entry to array
if (!is_array($this->config['headers'][$header_key])) {
$this->config['headers'][$header_key] = [
$this->config['headers'][$header_key]
];
}
$this->config['headers'][$header_key] = array_merge(
$this->config['headers'][$header_key],
$value
);
} else {
$this->config['headers'][$header_key] = $value;
}
} else {
$this->headers_named[strtolower($key)] = $key;
$this->config['headers'][$key] = $value;
}
}
}
/**
* remove header entry
* if key is only set then match only key, if both are set both sides must match
*
* @param array<string,null|string|array<string>> $remove_headers
* @return void
*/
public function removeHeaders(array $remove_headers): void
{
foreach ($remove_headers as $key => $value) {
if (!isset($this->headers_named[strtolower($key)])) {
continue;
}
$header_key = $this->headers_named[strtolower($key)];
if (!isset($this->config['headers'][$header_key])) {
continue;
}
// full remove
if (
empty($value) ||
(
(
// array both sides = equal
// string both sides = equal
(is_array($value) && is_array($this->config['headers'][$header_key])) ||
(is_string($value) && is_string($this->config['headers'][$header_key]))
) &&
$value == $this->config['headers'][$header_key]
)
) {
unset($this->config['headers'][$header_key]);
unset($this->headers_named[$header_key]);
} elseif (
// string value, array keys = in
// or both array and not a full match in the one before
(is_string($value) || is_array($value)) &&
is_array($this->config['headers'][$header_key])
) {
// part remove of key, value must be array
if (!is_array($value)) {
$value = [$value];
}
// array values so we rewrite the key pos
$this->config['headers'][$header_key] = array_values(array_diff(
$this->config['headers'][$header_key],
$value
));
}
}
}
// MARK: update/set base url
/**
* Update or set the base url set
* if empty will unset the base url
*
* @param string $base_uri
* @return void
*/
public function setBaseUri(string $base_uri): void
{
$this->config['base_uri'] = $base_uri;
$this->parsed_base_uri = [];
if (!empty($base_uri)) {
if (($parsed_base_uri = $this->parseUrl($base_uri)) !== false) {
$this->parsed_base_uri = $parsed_base_uri;
}
}
}
// MARK: main public call interface
/**
* combined set call for any type of request with options type parameters
*
* phpcs:disable Generic.Files.LineLength
* @param string $type
* @param string $url
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<string,mixed>} $options
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
* @throws \UnexpectedValueException on missing body data when body data is needed
* phpcs:enable Generic.Files.LineLength
*/
public function request(string $type, string $url, array $options = []): array
{
// can have
// - headers
// - query
// depending on type, must have (post/put/patch), optional for (delete)
// - body
$type = strtolower($type);
// check if we need a payload data set, set empty on not set
if (in_array($type, self::MANDATORY_POST_FIELDS) && !isset($options['body'])) {
$options['body'] = [];
}
return $this->curlRequest(
$type,
$url,
!array_key_exists('headers', $options) ? [] : $options['headers'],
$options['query'] ?? null,
$options['body'] ?? null
);
}
}
// __END__

View File

@@ -0,0 +1,157 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/10/29
* DESCRIPTION:
* Curl Client Trait for get/post/put/delete requests through the php curl inteface
*
* For anything more complex use guzzlehttp/http
* https://docs.guzzlephp.org/en/stable/index.html
*/
// phpcs:disable Generic.Files.LineLength
declare(strict_types=1);
namespace CoreLibs\UrlRequests;
trait CurlTrait
{
/**
* Set the array block that is sent to the request call
* Make sure that if headers is set as key but null it stays null and set to empty array
* if headers key is missing
* "get" calls do not set any body
*
* @param string $type if set as get do not add body, else add body
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>} $options Request options
* @return array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>}
*/
private function setOptions(string $type, array $options): array
{
if ($type == "get") {
return [
"headers" => !array_key_exists('headers', $options) ? [] : $options['headers'],
"query" => $options['query'] ?? null,
];
} else {
return [
"headers" => !array_key_exists('headers', $options) ? [] : $options['headers'],
"query" => $options['query'] ?? null,
"body" => $options['body'] ?? null,
];
}
}
/**
* combined set call for any type of request with options type parameters
* The following options can be set:
* header: as array string:string
* query as string or array string:string
* body as string or array of any type
*
* @param string $type What type of request we send, will throw exception if not a valid one
* @param string $url The url to send
* @param array{headers?:null|array<string,string|array<string>>,query?:null|string|array<string,mixed>,body?:null|string|array<string,mixed>} $options Request options
* @return array{code:string,headers:array<string,array<string>>,content:string} [default=[]] Result code, headers and content as array, content is json
* @throws \UnexpectedValueException on missing body data when body data is needed
*/
abstract public function request(string $type, string $url, array $options = []): array;
/**
* Makes an request to the target url via curl: GET
* Returns result as string (json)
*
* @param string $url The URL being requested,
* including domain and protocol
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>} $options Options to set
* @return array{code:string,headers:array<string,array<string>>,content:string} [default=[]] Result code, headers and content as array, content is json
*/
public function get(string $url, array $options = []): array
{
return $this->request(
"get",
$url,
$this->setOptions('get', $options),
);
// array{headers?: array<string, array<string>|string>|null, query?: array<string, string>|null, body?: array<string, mixed>|string|null},
// array{headers?: array<string, array<string>|string>|null, query?: array<string, mixed>|string|null, body?: array<string, mixed>|string|null}
}
/**
* Makes an request to the target url via curl: POST
* Returns result as string (json)
*
* @param string $url The URL being requested,
* including domain and protocol
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>} $options Options to set
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
*/
public function post(string $url, array $options): array
{
return $this->request(
"post",
$url,
$this->setOptions('post', $options),
);
}
/**
* Makes an request to the target url via curl: PUT
* Returns result as string (json)
*
* @param string $url The URL being requested,
* including domain and protocol
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>} $options Options to set
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
*/
public function put(string $url, array $options): array
{
return $this->request(
"put",
$url,
$this->setOptions('put', $options),
);
}
/**
* Makes an request to the target url via curl: PATCH
* Returns result as string (json)
*
* @param string $url The URL being requested,
* including domain and protocol
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>} $options Options to set
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
*/
public function patch(string $url, array $options): array
{
return $this->request(
"patch",
$url,
$this->setOptions('patch', $options),
);
}
/**
* Makes an request to the target url via curl: DELETE
* Returns result as string (json)
* Note that DELETE body is optional
*
* @param string $url The URL being requested,
* including domain and protocol
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>} $options Options to set
* @return array{code:string,headers:array<string,array<string>>,content:string} [default=[]] Result code, headers and content as array, content is json
*/
public function delete(string $url, array $options = []): array
{
return $this->request(
"delete",
$url,
$this->setOptions('delete', $options),
);
}
}
// __END__

View File

@@ -0,0 +1,95 @@
<?php
/**
* AUTHOR: Clemens Schwaighofer
* CREATED: 2024/9/20
* DESCRIPTION:
* URL Requests client interface
*/
namespace CoreLibs\UrlRequests\Interface;
interface RequestsInterface
{
/**
* Convert an array with header strings like "foo: bar" to the interface
* needed "foo" => "bar" type
* Skips entries that are already in key => value type, by checking if the
* key is a not a number
*
* @param array<int|string,string> $headers
* @return array<string,string>
* @throws \UnexpectedValueException on duplicate header key
*/
public function prepareHeaders(array $headers): array;
/**
* get the config array with all settings
*
* @return array<string,mixed> all current config settings
*/
public function getConfig(): array;
/**
* Return the full url as it was sent
*
* @return string url sent
*/
public function getUrlSent(): string;
/**
* get the parsed url
*
* @return array{scheme?:string,user?:string,host?:string,port?:string,path?:string,query?:string,fragment?:string,pass?:string}
*/
public function getUrlParsedSent(): array;
/**
* Return the full headers as they where sent
*
* @return array<string,string>
*/
public function getHeadersSent(): array;
/**
* set, add or overwrite header
* On default this will overwrite header, and not set
*
* @param array<string,string|array<string>> $header
* @param bool $add [default=false] if set will add header to existing value
* @return void
*/
public function setHeaders(array $header, bool $add = false): void;
/**
* remove header entry
* if key is only set then match only key, if both are set both sides must match
*
* @param array<string,string> $remove_headers
* @return void
*/
public function removeHeaders(array $remove_headers): void;
/**
* Update the base url set, if empty will unset the base url
*
* @param string $base_uri
* @return void
*/
public function setBaseUri(string $base_uri): void;
/**
* combined set call for any type of request with options type parameters
*
* phpcs:disable Generic.Files.LineLength
* @param string $type
* @param string $url
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<string,mixed>} $options
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
* @throws \UnexpectedValueException on missing body data when body data is needed
* phpcs:enable Generic.Files.LineLength
*/
public function request(string $type, string $url, array $options = []): array;
}
// __END__