diff --git a/README.md b/README.md index c86af13..376af5a 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,139 @@ # Reflect API client for PHP -Make requests to an API built using the [Reflect API](https://github.com/VictorWesterlund/reflect) framework over HTTP or UNIX sockets. This program comes with both an extendable/instantiable class that you can integrate with existing PHP code, or as a stand-alone CLI which can be run by itself or UNIX piped into other programs. +Make requests from a PHP application to an API built with the [Reflect API framework](https://github.com/VictorWesterlund/reflect). ---- - -Make a request with `Client->call()`. It will always return the response as an array of length 2. -- The first value is the HTTP-equivalent response code. -- The second value is the response body - -```php -$client = new Reflect\Client("", ""); - -$client->call("foo", Reflect\Method::GET); // (array) [200, "bar"] -$client->call("foo", Reflect\Method::POST, [ - "foo" => "bar" -]); // (array) [201, "Created"] - -// etc.. -``` - -## How to use - -Requires PHP 8.1 or newer, and of course a Reflect API endpoint. +## Installation 1. **Install with composer** - - ``` - composer require reflect/client - ``` - -2. **Initialize the class** - - ```php - require_once "/vendor/autoload.php"; - - $client = new Reflect\Client("", ""); - ``` - -3. **Make API request** - - Use the `call()` method to perform a request - - ```php - $client->call("foo?bar=baz", Reflect\Method::GET); - ``` - - Argument index|Type|Required|Description - --|--|--|-- - 0|String|Yes|Fully qualified pathname and query params of the endpoint to call - 1|Method\|string|Yes|A supported [Reflect HTTP method](https://github.com/VictorWesterlund/reflect/wiki/Supported-technologies#http-request-methods) (eg. `Method::GET`) or a string represnetation of a supported method (eg. "GET") - 2|Array|No|An optional indexed, associative, or multidimensional array that will be sent as the request body as `Content-Type: application/json` - - The `call()` function will return an array of length 2 wil the following information - - Index|Type|Description - --|--|-- - 0|Int|HTTP-equivalent response code (eg. `200` or `404`) - 1|String/Array|Contains the response body as either a string, or array if the response `Content-Type` header is `application/json` - -## How to use (CLI) - -You can also run this from the command line with - ``` -php client [payload] +composer require reflect/client ``` -and it will return a serialized JSON array with the same structure as described in the `Client->call()` return. +2. **`use` in your PHP code** +```php +use Reflect\Client; -*Example* -```sh -php client "/run/reflect.sock" "foo?bar=biz" "POST" "[\"foo\" => \"bar\"]" # (string) [201, \"Created\"] +$client = new Client(string $api_base_url, ?string $api_key, ?bool $verify_peer = true); ``` ---- +## Making API calls -Requires PHP CLI 8.1 or greater, and of course a Reflect API endpoint. +Start by initializing the `Client` with a base URL to the API, and with an optional API key. -1. **Clone repo** +```php +use Reflect\Client; +use Reflect\Method; - ``` - git clone https://github.com/victorwesterlund/reflect-client-php - ``` - -2. **Run from command line** +$client = new Client("https://api.example.com", "MyApiKey"); +``` - ``` - // From the root directory of this project - php client [payload] - ``` +### Defining an endpoint + +Start a new API call by chaining the `call()` method and passing it an endpoint string + +```php +Client->call(string $endpoint): self +``` + +Example: + +```php +$client->call("my/endpoint"); +``` + +### (Optional) Search Parameters + +Pass an associative array of keys and values to `params()`, and chain it anywhere before a `get()`, `patch()`, `put()`, `post()`, or `delete()` request to set search (`$_GET`) parameters for the current request. + +```php +Client->params(?array $params = null): self +``` + +Example: + +```php +// https://api.example.com/my/endpoint?key1=value1&key2=value2 +$client->call("my/endpoint") + ->params([ + "key1" => "value1", + "key2" => "value2" + ]); +``` + +### `GET` Request + +Make a `GET` request by chaining `get()` at the end of a method chain. This method will return a `Reflect\Response` object. + +```php +Client->get(): Reflect\Response; +``` + +Example: + +```php +$client->call("my/endpoint")->params(["foo" => "bar"])->get(); +``` + +### `POST` Request + +Make a `POST` request by chaining `post()` at the end of a method chain. This method will return a `Reflect\Response` object. + +Pass `post()` a stringifiable associative array of key, value pairs to be sent as an `application/json`-encoded request body to the endpoint. + +```php +Client->post(array $payload): Reflect\Response; +``` + +Example: + +```php +$client->call("my/endpoint")->params(["foo" => "bar"])->post(["baz" => "qux"]); +``` + +### `PATCH` Request + +Make a `PATCH` request by chaining `patch()` at the end of a method chain. This method will return a `Reflect\Response` object. + +Pass `patch()` a stringifiable associative array of key, value pairs to be sent as an `application/json`-encoded request body to the endpoint. + +```php +Client->patch(array $payload): Reflect\Response; +``` + +Example: + +```php +$client->call("my/endpoint")->params(["foo" => "bar"])->patch(["baz" => "qux"]); +``` + +### `PUT` Request + +Make a `PUT` request by chaining `put()` at the end of a method chain. This method will return a `Reflect\Response` object. + +Pass `put()` a stringifiable associative array of key, value pairs to be sent as an `application/json`-encoded request body to the endpoint. + +```php +Client->put(array $payload): Reflect\Response; +``` + +Example: + +```php +$client->call("my/endpoint")->params(["foo" => "bar"])->put(["baz" => "qux"]); +``` + +### `DELETE` Request + +Make a `DELETE` request by chaining `delete()` at the end of a method chain. This method will return a `Reflect\Response` object. + +Pass `delete()` an optional stringifiable associative array of key, value pairs to be sent as an `application/json`-encoded request body to the endpoint. + +```php +Client->delete(?array $payload = null): Reflect\Response; +``` + +Example: + +```php +$client->call("my/endpoint")->params(["foo" => "bar"])->delete(); +``` diff --git a/client b/client deleted file mode 100644 index a8f22d7..0000000 --- a/client +++ /dev/null @@ -1,32 +0,0 @@ - 4) { - $arglen = $argc - 1; - exit("Expected 3 to 4 arguments (got ${arglen}): [payload]\n"); - } - - // Connect to the socket server - $client = new Client($argv[1], null, Connection::AF_UNIX); - - // Get endpoint, method, and optional payload - $args = $argv; - array_shift($args); - array_shift($args); - - // Restore enum from argument - $args[1] = Method::from(strtoupper($args[1])); - - // Call endpoint and echo result - $call = $client->call(...$args); - echo json_encode($call) . "\n"; \ No newline at end of file diff --git a/src/Reflect/Client.php b/src/Reflect/Client.php index a9c286a..baeee7d 100644 --- a/src/Reflect/Client.php +++ b/src/Reflect/Client.php @@ -2,71 +2,36 @@ namespace Reflect; - // Allowed HTTP verbs - enum Method: string { - case GET = "GET"; - case POST = "POST"; - case PUT = "PUT"; - case DELETE = "DELETE"; - case PATCH = "PATCH"; - case OPTIONS = "OPTIONS"; - } + use Reflect\Method; + use Reflect\Response; - // Supported connection methods - enum Connection { - case HTTP; - case AF_UNIX; - } + require_once "Method.php"; + require_once "Response.php"; class Client { - // API key string - private ?string $key; - // Connection method - private Connection $con; - // Request endpoitn string - private string $endpoint; + protected ?string $key; + protected string $base_url; + protected bool $https_verify_peer; - // Socket instance - private Socket|false $socket = false; - // Flag: Allow unverified SSL certificates for HTTPS - private bool $https_peer_verify = true; - - // Use this HTTP method if no method specified to call() - const HTTP_DEFAULT_METHOD = Method::GET; - // The amount of bytes to read for each chunk from socket - const SOCKET_READ_BYTES = 2048; - - public function __construct(string $endpoint, string $key = null, Connection $con = null, bool $https_peer_verify = true) { - $this->con = $con ?: $this::resolve_connection($endpoint); - $this->endpoint = $endpoint; + protected string $endpoint; + protected string $params; + + public function __construct(string $base_url, string $key = null, bool $verify_peer = true) { + // Optional API key $this->key = $key; - if ($this->con === Connection::AF_UNIX) { - // Connect to Reflect UNIX socket - $this->_socket = socket_create(AF_UNIX, SOCK_STREAM, 0); - $conn = socket_connect($this->_socket, $this->endpoint); - } else if ($this->con === Connection::HTTP) { - // Append tailing "/" for HTTP if absent - $this->endpoint = substr($this->endpoint, -1) === "/" ? $this->endpoint : $this->endpoint . "/"; - // Flag which enables or disables SSL peer validation (for self-signed certificates) - $this->https_peer_verify = $https_peer_verify; - } + // Append tailing "/" if absent + $this->base_url = substr($this->base_url, -1) === "/" ? $this->base_url : $this->base_url . "/"; + // Flag which enables or disables SSL peer validation (for self-signed certificates) + $this->https_verify_peer = $verify_peer; } - // Resolve connection type from endpoint string. - // If the string is a valid URL we will treat it as HTTP otherwise we will assume it's a path on disk to a UNIX socket file. - private static function resolve_connection(string $endpoint): Connection { - return filter_var($endpoint, FILTER_VALIDATE_URL) - ? Connection::HTTP - : Connection::AF_UNIX; + // Convert assoc array to URL-encoded string or empty string if array is empty + private static function get_params(array $params): string { + return !empty($params) ? "?" . http_build_query($params) : ""; } - // Attempt to resolve Method from backed enum string, or return default - private static function resolve_method(Method|string $method): Method { - return ($method instanceof Method) - ? $method - : Method::tryFrom($method) ?? (__CLASS__)::HTTP_DEFAULT_METHOD; - } + // ---- // Construct stream_context_create() compatible header string private function http_headers(): string { @@ -83,67 +48,72 @@ } // Make request and return response over HTTP - private function http_call(string $endpoint, Method|string $method = (__CLASS__)::HTTP_DEFAULT_METHOD, array $payload = null): array { - // Resolve string to enum - $method = $this::resolve_method($method); - // Remove leading "/" if present, as it's already present in $this->endpoint - $endpoint = substr($endpoint, 0, 1) !== "/" ? $endpoint : substr($endpoint, 1, strlen($endpoint) - 1); - - $data = stream_context_create([ + private function http_call(Method $method, array $payload = null): array { + $context = stream_context_create([ "http" => [ "header" => $this->http_headers(), - "method" => $method->value, + "method" => $method->name, "ignore_errors" => true, "content" => !empty($payload) ? json_encode($payload) : "" ], "ssl" => [ - "verify_peer" => $this->https_peer_verify, - "verify_peer_name" => $this->https_peer_verify, - "allow_self_signed" => !$this->https_peer_verify + "verify_peer" => $this->https_verify_peer, + "verify_peer_name" => $this->https_verify_peer, + "allow_self_signed" => !$this->https_verify_peer ] ]); - $resp = file_get_contents($this->endpoint . $endpoint, false, $data); + $resp = file_get_contents($this->base_url . $this->endpoint, false, $context); // Get HTTP response code from $http_response_header which materializes out of thin air after file_get_contents(). // The first header line and second word will contain the status code. $resp_code = (int) explode(" ", $http_response_header[0])[1]; - // Return response as [, ] - return [$resp_code, json_decode($resp, true)]; + // Return response as [, ] + return [$resp, $resp_code]; } - // Make request and return response over socket - private function socket_txn(string $payload): string { - $tx = socket_write($this->_socket, $payload, strlen($payload)); - $rx = socket_read($this->_socket, $this::SOCKET_READ_BYTES); + // ---- - if (!$tx || !$rx) { - throw new \Error("Failed to complete transaction"); - } - - return $rx; + // Construct URL search parameters from array if set + public function params(?array $params = null): self { + $this->params = !empty($params) ? self::get_params($params) : ""; + return $this; } - // Call a Reflect endpoint and return response as assoc array - public function call(string $endpoint, Method|string $method = (__CLASS__)::HTTP_DEFAULT_METHOD, array $payload = null): array { - // Resolve string to enum - $method = $this::resolve_method($method); + // Create a new call to an endpoint + public function call(string $endpoint): self { + // Remove leading "/" if present, as it's already present in $this->base_url + $this->endpoint = substr($this->endpoint, 0, 1) !== "/" + ? $this->endpoint + : substr($this->endpoint, 1, strlen($this->endpoint) - 1); - // Call endpoint over UNIX socket - if ($this->con === Connection::AF_UNIX) { - // Return response as assoc array - return json_decode($this->socket_txn( - // Send request as stringified JSON - json_encode([ - $endpoint, - $method->value, - $payload - ]) - ), true); - } + // Reset search parameters + $this->params(); - // Call endpoint over HTTP - return $this->http_call(...func_get_args()); + return $this; + } + + // ---- + + // Make a GET request to endpoint with optional search parameters + public function get(): Response { + return new Response(...$this->http_call(Method::GET)); + } + + public function patch(array $payload): Response { + return new Response(...$this->http_call(Method::PATCH, $payload)); + } + + public function put(array $payload): Response { + return new Response(...$this->http_call(Method::PUT, $payload)); + } + + public function post(array $payload): Response { + return new Response(...$this->http_call(Method::POST, $payload)); + } + + public function delete(?array $payload = []): Response { + return new Response(...$this->http_call(Method::DELETE, $payload)); } } \ No newline at end of file diff --git a/src/Reflect/Method.php b/src/Reflect/Method.php new file mode 100644 index 0000000..55d735e --- /dev/null +++ b/src/Reflect/Method.php @@ -0,0 +1,13 @@ +body = is_array($resp) ? $resp[0] : $resp; + // Set response code from array or with method argument + $this->code = is_array($resp) ? (int) $resp[1] : $code; + + // Response code is within the Success range + $this->ok = $this->code >= 200 && $this->code < 300; + } + + // Parse JSON from response body and return as PHP array + public function json(bool $assoc = true): array { + return json_decode($this->body, $assoc); + } + + // Return response body as-is + public function output() { + return $this->body; + } + } \ No newline at end of file