From daf75371e3fa245c72dd4b93494e26be127cb7e7 Mon Sep 17 00:00:00 2001 From: Lessmore92 Date: Sun, 2 Feb 2020 23:40:01 +0330 Subject: [PATCH] This is where it all begins... --- composer.json | 21 ++ src/ApiConsumer.php | 83 +++++ src/Builders/ApiBuilder.php | 72 ++++ src/Builders/RequestBuilder.php | 212 ++++++++++++ src/Builders/ResponseBuilder.php | 137 ++++++++ src/Contracts/AbstractApiBuilder.php | 19 ++ src/Contracts/AbstractRequestBuilder.php | 19 ++ src/Contracts/AbstractResponseBuilder.php | 18 + src/Contracts/HttpClientInterface.php | 50 +++ src/Contracts/ModelInterface.php | 17 + src/Contracts/RequestModelInterface.php | 19 ++ .../ApiKeyParamNameMustBeDefine.php | 15 + src/Exceptions/BadResponseException.php | 13 + src/Exceptions/ClientException.php | 23 ++ .../ConfigApiNotReturnApiBuilder.php | 15 + src/Exceptions/ConnectException.php | 13 + src/Exceptions/RequestException.php | 15 + src/Exceptions/RequestMethodNotSupported.php | 15 + src/Exceptions/RequestNotValid.php | 15 + src/Exceptions/ServerException.php | 13 + src/Foundation/Collection.php | 67 ++++ src/Foundation/Model.php | 312 ++++++++++++++++++ src/Foundation/ModelObjectCache.php | 29 ++ src/Foundation/RequestDirector.php | 255 ++++++++++++++ src/Foundation/RequestModel.php | 249 ++++++++++++++ src/Foundation/ResponseModel.php | 20 ++ src/Foundation/Singleton.php | 61 ++++ src/Foundation/StatusCodes.php | 165 +++++++++ src/HttpClients/Curl.php | 174 ++++++++++ src/Models/Api.php | 24 ++ src/Models/RawResponse.php | 22 ++ src/Models/Request.php | 15 + src/Models/Response.php | 21 ++ 33 files changed, 2218 insertions(+) create mode 100644 composer.json create mode 100644 src/ApiConsumer.php create mode 100644 src/Builders/ApiBuilder.php create mode 100644 src/Builders/RequestBuilder.php create mode 100644 src/Builders/ResponseBuilder.php create mode 100644 src/Contracts/AbstractApiBuilder.php create mode 100644 src/Contracts/AbstractRequestBuilder.php create mode 100644 src/Contracts/AbstractResponseBuilder.php create mode 100644 src/Contracts/HttpClientInterface.php create mode 100644 src/Contracts/ModelInterface.php create mode 100644 src/Contracts/RequestModelInterface.php create mode 100644 src/Exceptions/ApiKeyParamNameMustBeDefine.php create mode 100644 src/Exceptions/BadResponseException.php create mode 100644 src/Exceptions/ClientException.php create mode 100644 src/Exceptions/ConfigApiNotReturnApiBuilder.php create mode 100644 src/Exceptions/ConnectException.php create mode 100644 src/Exceptions/RequestException.php create mode 100644 src/Exceptions/RequestMethodNotSupported.php create mode 100644 src/Exceptions/RequestNotValid.php create mode 100644 src/Exceptions/ServerException.php create mode 100644 src/Foundation/Collection.php create mode 100644 src/Foundation/Model.php create mode 100644 src/Foundation/ModelObjectCache.php create mode 100644 src/Foundation/RequestDirector.php create mode 100644 src/Foundation/RequestModel.php create mode 100644 src/Foundation/ResponseModel.php create mode 100644 src/Foundation/Singleton.php create mode 100644 src/Foundation/StatusCodes.php create mode 100644 src/HttpClients/Curl.php create mode 100644 src/Models/Api.php create mode 100644 src/Models/RawResponse.php create mode 100644 src/Models/Request.php create mode 100644 src/Models/Response.php diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a24f780 --- /dev/null +++ b/composer.json @@ -0,0 +1,21 @@ +{ + "name": "lessmore92/api-consumer", + "description": "Build REST API consumer easier than ever", + "type": "package", + "require": { + "php": ">=5.5" + }, + "license": "mit", + "authors": [ + { + "name": "Mojtaba Bahrami", + "email": "bahramisoft@gmail.com" + } + ], + "minimum-stability": "dev", + "autoload": { + "psr-4": { + "Lessmore92\\ApiConsumer\\": "src/" + } + } +} diff --git a/src/ApiConsumer.php b/src/ApiConsumer.php new file mode 100644 index 0000000..02c0491 --- /dev/null +++ b/src/ApiConsumer.php @@ -0,0 +1,83 @@ +http = $httpClient; + $this->request_builder = new RequestBuilder(); + $this->response_builder = new ResponseBuilder(); + + $api = $this->ConfigApi(); + + if (!($api instanceof ApiBuilder)) + { + throw new ConfigApiNotReturnApiBuilder('ConfigApi() must return an instance of \'Lessmore92\ApiConsumer\Builders\ApiBuilder\''); + } + + $this->api = $api->getApi(); + + $this->request_builder->setApi($this->api); + + $this->request_director = new RequestDirector($this->request_builder, $this->response_builder, $httpClient); + } + + /** + * @return ApiBuilder + */ + abstract protected function ConfigApi(); + + /** + * @return RequestDirector + */ + public function Request() + { + return $this->request_director; + } +} diff --git a/src/Builders/ApiBuilder.php b/src/Builders/ApiBuilder.php new file mode 100644 index 0000000..2cbaab4 --- /dev/null +++ b/src/Builders/ApiBuilder.php @@ -0,0 +1,72 @@ +api = new Api(); + } + + /** + * @return Api + */ + public function getApi() + { + return $this->api; + } + + /** + * @param string $api_key + * @param string $api_key_param_name + * @return $this + */ + public function setHeaderApiKey($api_key, $api_key_param_name = 'x-api-key') + { + return $this->setApiKey($api_key, $api_key_param_name, 'header'); + } + + private function setApiKey($api_key, $api_key_param_name = 'x-api-key', $api_key_place = 'header') + { + if (trim($api_key_param_name) === '') + { + throw new ApiKeyParamNameMustBeDefine('api_key_param_name must be defined, empty string not allowed'); + } + $this->api->api_key_place = $api_key_place; + $this->api->api_key_param_name = $api_key_param_name; + $this->api->api_key = $api_key; + return $this; + } + + /** + * @param string $api_key + * @param string $api_key_param_name + * @return $this + */ + public function setQueryApiKey($api_key, $api_key_param_name = 'api_key') + { + return $this->setApiKey($api_key, $api_key_param_name, 'query_string'); + } + + /** + * @param string $url + * @return $this + */ + public function setBaseUrl($url) + { + $this->api->base_url = $url; + return $this; + } +} diff --git a/src/Builders/RequestBuilder.php b/src/Builders/RequestBuilder.php new file mode 100644 index 0000000..f689639 --- /dev/null +++ b/src/Builders/RequestBuilder.php @@ -0,0 +1,212 @@ +request = new Request(); + $this->api = $api; + + $this->init(); + } + + private function init() + { + if ($this->api === null) + { + return; + } + + if ($this->api->base_url !== '') + { + $this->request->url = $this->api->base_url; + } + + if ($this->api->api_key !== '') + { + if ($this->api->api_key_place === Api::API_KEY_IN_QUERY_STRING) + { + $this->request->addQueryString($this->api->api_key_param_name, $this->api->api_key); + } + else if ($this->api->api_key_place === Api::API_KEY_IN_HEADER) + { + $this->request->addHeader($this->api->api_key_param_name, $this->api->api_key); + } + //TODO throw exception api key place not valid + } + } + + public function setApi($api) + { + $this->api = $api; + $this->init(); + } + + /** + * @return Request + */ + public function buildRequest() + { + $request = unserialize(serialize($this->request)); + $this->reset(); + return $request; + } + + private function reset() + { + $this->endPoint(''); + $this->setBody(''); + $this->request->clearOnetimeHeaders(); + $this->request->clearOnetimeQueryStrings(); + } + + /** + * @param string $endpoint + * @return RequestBuilder + */ + public function endPoint($endpoint) + { + $this->request->path = $endpoint; + return $this; + } + + /** + * @param string $body + * @return RequestBuilder + */ + public function setBody($body) + { + $this->request->is_json = false; + $this->request->body = $body; + return $this; + } + + /** + * @param array $body + * @return RequestBuilder + */ + public function setJsonBody($body) + { + $this->request->is_json = true; + $this->request->json_body = $body; + return $this; + } + + /** + * + * @param string $key + * @param string $value + * @param bool $onetime if true header remove after buildRequest() called + * @return RequestBuilder + */ + public function addHeader($key, $value, $onetime = false) + { + if ($onetime) + { + $this->request->addOnetimeHeader($key, $value); + } + else + { + $this->request->addHeader($key, $value); + } + return $this; + } + + /** + * + * @param string $key + * @param bool $onetime if true header removed from onetime headers + * @return RequestBuilder + */ + public function removeHeader($key, $onetime = false) + { + if ($onetime) + { + $this->request->removeOnetimeHeader($key); + } + else + { + $this->request->removeHeader($key); + } + return $this; + } + + /** + * @param string $key + * @param string $value + * @param bool $onetime if true query_string remove after buildRequest() called + * @return RequestBuilder + */ + public function addQueryString($key, $value, $onetime = false) + { + if ($onetime) + { + $this->request->addOnetimeQueryString($key, $value); + } + else + { + $this->request->addQueryString($key, $value); + } + return $this; + } + + /** + * + * @param string $key + * @param bool $onetime if true query_string removed from onetime query_strings + * @return RequestBuilder + */ + public function removeQueryString($key, $onetime = false) + { + if ($onetime) + { + $this->request->removeOnetimeQueryString($key); + } + else + { + $this->request->removeQueryString($key); + } + return $this; + } + + /** + * @param string $method + * @return RequestBuilder + */ + public function setMethod($method) + { + if (!defined('\Lessmore92\ApiConsumer\Contracts\RequestModelInterface::REQUEST_METHOD_' . strtoupper($method))) + { + throw new RequestMethodNotSupported(sprintf('Request method %s not supported.', $method)); + } + $this->request->method = $method; + return $this; + } +} diff --git a/src/Builders/ResponseBuilder.php b/src/Builders/ResponseBuilder.php new file mode 100644 index 0000000..b642e54 --- /dev/null +++ b/src/Builders/ResponseBuilder.php @@ -0,0 +1,137 @@ +response = new Response(); + } + + /** + * @param RawResponse $raw + * @return Response + * @throws BadResponseException + * @throws ClientException + * @throws ServerException + */ + public function fromRawResponse(RawResponse $raw) + { + $header_size = $raw->info['header_size']; + $_header = explode("\n", trim(mb_substr($raw->response, 0, $header_size))); + $status_line = array_shift($_header); + $body = trim(mb_substr($raw->response, $header_size)); + $status_code = $raw->info['http_code']; + $status_message = $this->get_http_status_message($status_line); + + $headers = array(); + foreach ($_header as $line) + { + //TODO handle redirected request headers + list($key, $val) = explode(':', $line, 2); + $headers[strtolower($key)] = trim($val); + } + + if ($this->is_server_error($status_code)) + { + throw new ServerException($this->build_exception_message($status_code, $status_message)); + } + + if ($this->is_client_error($status_code)) + { + $exception = new ClientException($this->build_exception_message($status_code, $status_message)); + $exception->setBody($body); + throw $exception; + } + + if (!$this->is_success($status_code)) + { + throw new BadResponseException($this->build_exception_message($status_code, $status_message)); + } + + $response = new Response(); + + $response->body = $body; + $response->headers = $headers; + $response->status_code = $status_code; + $response->status_message = $status_message; + + + if (isset($headers['content-type'])) + { + $content_type = $headers['content-type']; + $is_json = stripos($content_type, 'json') !== false; + if ($is_json) + { + $response->json_body = @json_decode($body, true); + } + } + + return $response; + } + + /** + * @param string $status_line + * @return string + */ + private function get_http_status_message($status_line) + { + $re = '/HTTP\/[0-9\.]+ ([0-9]+) ([0-9a-z ]+)?/mi'; + + preg_match_all($re, $status_line, $matches, PREG_SET_ORDER, 0); + + return (is_array($matches) && count($matches) > 0 && count($matches[0]) > 2) ? $matches[0][2] : ''; + } + + private function is_server_error($status_code) + { + return $status_code >= 500 && $status_code <= 599; + } + + private function build_exception_message($code, $message = '') + { + if (trim($message) === '') + { + $message = StatusCodes::getMessageForCode($code); + } + return sprintf('%s %s', $code, $message); + } + + private function is_client_error($status_code) + { + return $status_code >= 400 && $status_code <= 499; + } + + private function is_success($status_code) + { + return $status_code >= 200 && $status_code <= 299; + } + + /** + * @return response + */ + public function buildResponse() + { + return $this->response; + } +} diff --git a/src/Contracts/AbstractApiBuilder.php b/src/Contracts/AbstractApiBuilder.php new file mode 100644 index 0000000..ba3ff6e --- /dev/null +++ b/src/Contracts/AbstractApiBuilder.php @@ -0,0 +1,19 @@ +body; + } + + public function setBody($body) + { + $this->body = $body; + } +} diff --git a/src/Exceptions/ConfigApiNotReturnApiBuilder.php b/src/Exceptions/ConfigApiNotReturnApiBuilder.php new file mode 100644 index 0000000..95e1ca8 --- /dev/null +++ b/src/Exceptions/ConfigApiNotReturnApiBuilder.php @@ -0,0 +1,15 @@ +items = $items; + } + + public function filter(callable $callback) + { + return array_filter($this->items, $callback, ARRAY_FILTER_USE_BOTH); + } + + public function offsetExists($key) + { + return array_key_exists($key, $this->items); + } + + public function offsetGet($key) + { + return $this->items[$key]; + } + + public function offsetSet($key, $value) + { + if ($key === null) + { + $this->items[] = $value; + } + else + { + $this->items[$key] = $value; + } + } + + public function offsetUnset($key) + { + unset($this->items[$key]); + } + + + public function getIterator() + { + return new ArrayIterator($this->items); + } + + public function count() + { + return count($this->items); + } +} diff --git a/src/Foundation/Model.php b/src/Foundation/Model.php new file mode 100644 index 0000000..1243d3e --- /dev/null +++ b/src/Foundation/Model.php @@ -0,0 +1,312 @@ +caller_class = get_called_class(); + $this->fire(); + foreach ((array)$attributes as $key => $attribute) + { + $this->{$key} = $attribute; + } + } + + protected function fire() + { + $this->parse_properties(); + } + + private function parse_properties($doc = null) + { + if (empty($this->notations)) + { + if (is_null($doc)) + { + $doc = ''; + $reflection = new ReflectionClass($this); + do + { + if (is_subclass_of($reflection->name, ModelContract::class)) + { + $doc .= $reflection->getDocComment(); + } + + } while ($reflection = $reflection->getParentClass()); + } + + $notations = $this->extract_notations($doc); + $notations = $this->join_multiline_notations($notations); + foreach ($notations as $_notation) + { + if (!in_array($_notation['tag'], $this->tags)) + { + continue; + } + $notation = $this->parse_tag($_notation['value']); + $this->notations[$notation['name']] = ['type' => $notation['type'], 'is_array' => $notation['is_array']]; + } + } + } + + /** + * Extract notation from doc comment + * + * @param string $doc + * @return array + */ + private function extract_notations($doc) + { + $matches = null; + $tag = '\s*@(?\S+)(?:\h+(?\S.*?)|\h*)'; + $tagContinue = '(?:\040){2}(?\S.*?)'; + $regex = '/^\s*(?:(?:\/\*)?\*)?(?:' . $tag . '|' . $tagContinue . ')(?:\*\*\/)?\r?$/m'; + return preg_match_all($regex, $doc, $matches, PREG_SET_ORDER) ? $matches : []; + } + + /** + * Join multiline notations + * + * @param array $rawNotations + * @return array + */ + private function join_multiline_notations($rawNotations) + { + $result = []; + $tagsNotations = $this->filter_tags_notations($rawNotations); + foreach ($tagsNotations as $item) + { + if (!empty($item['tag'])) + { + $result[] = $item; + } + else + { + $lastIdx = count($result) - 1; + $result[$lastIdx]['value'] = trim($result[$lastIdx]['value']) + . ' ' . trim($item['multiline_value']); + } + } + return $result; + } + + /** + * Remove everything that goes before tags + * + * @param array $rawNotations + * @return array + */ + private function filter_tags_notations($rawNotations) + { + $count = count($rawNotations); + for ($i = 0; $i < $count; $i++) + { + if (!empty($rawNotations[$i]['tag'])) + { + return array_slice($rawNotations, $i); + } + } + return []; + } + + ////////////////////DcoParser + + private function parse_tag($tag) + { + $is_array = false; + list($type, $name) = explode(' ', $tag); + + if (stripos($type, '[]') !== false) + { + $type = str_ireplace('[]', '', $type); + $is_array = true; + } + $out['type'] = $type; + $out['name'] = $name; + $out['is_array'] = $is_array; + + return $out; + } + + public function &__get($key) + { + $value = null; + if (isset($this->attributes[$key])) + { + $value = $this->attributes[$key]; + } + + $value = $this->executeCustomGet($key, $value); + return $value; + } + + public function __set($key, $value) + { + if (!$this->should_be_added_to_model($key)) + { + return; + } + $value = $this->executeCustomSet($key, $value); + $this->attributes[$key] = $this->cast_value($key, $value); + } + + private function should_be_added_to_model($key) + { + if ($this->lock && !isset($this->notations[$key])) + { + return false; + } + return true; + } + + protected function executeCustomSet($key, $value) + { + $_value = $value; + if ($this->hasCustomSet($key)) + { + $_value = $this->{"set" . $key . "attribute"}($value); + } + return $_value; + } + + protected function hasCustomSet($key) + { + return method_exists($this, "set" . $key . "attribute"); + } + + private function cast_value($key, $value) + { + $_value = $value; + $cache = ModelObjectCache::instance() + ->getCache($this->caller_class, 'cast_' . $key) + ; + + if ($cache) + { + $type = $cache; + } + else + { + $type = $this->cast_type($key); + ModelObjectCache::instance() + ->setCache($this->caller_class, 'cast_' . $key, $type) + ; + } + + + if ($type['type'] && $type['is_native']) + { + settype($_value, $type['type']); + } + else if ($type['type'] && $type['is_array']) + { + $_value = []; + foreach ((array)$value as $item) + { + $_value[] = new $type['type']($item); + } + } + elseif ($type['type']) + { + $_value = new $type['type']($value); + } + + return $_value; + } + + private function cast_type($key) + { + $type = [ + 'type' => null, + 'is_array' => false, + 'is_native' => false, + ]; + if (isset($this->notations[$key])) + { + set_error_handler([$this, 'ignore_warnings'], E_WARNING); + if (class_exists($this->notations[$key]['type'])) + { + if ($this->notations[$key]['is_array']) + { + $type['type'] = $this->notations[$key]['type']; + $type['is_array'] = true; + } + else + { + $type['type'] = $this->notations[$key]['type']; + } + } + else + { + $type['type'] = $this->notations[$key]['type']; + $type['is_native'] = true; + + } + restore_error_handler(); + } + return $type; + } + + protected function executeCustomGet($key, $value) + { + $_value = $value; + if (method_exists($this, "get" . $key . "attribute")) + { + $_value = $this->{"get" . $key . "attribute"}($value); + } + return $_value; + } + + /** + * @return array + */ + public function toArray() + { + $out = []; + foreach ($this->attributes as $key => $attribute) + { + if (is_array($attribute) && isset($attribute[0]) && $attribute[0] instanceof ModelContract) + { + foreach ($attribute as $_key => $item) + { + $out[$key][$_key] = $item->toArray(); + } + } + else if ($attribute instanceof ModelContract) + { + $out[$key] = $attribute->toArray(); + } + else + { + //force run __get + $out[$key] = $this->{$key}; + } + } + return $out; + } + + public function ignore_warnings($error_no, $error_str) + { + + } +} diff --git a/src/Foundation/ModelObjectCache.php b/src/Foundation/ModelObjectCache.php new file mode 100644 index 0000000..4e0b314 --- /dev/null +++ b/src/Foundation/ModelObjectCache.php @@ -0,0 +1,29 @@ +objects[$class][$key])) + { + return $this->objects[$class][$key]; + } + + return null; + } + + public function setCache($class, $key, $obj) + { + $this->objects[$class][$key] = $obj; + } +} diff --git a/src/Foundation/RequestDirector.php b/src/Foundation/RequestDirector.php new file mode 100644 index 0000000..b2fad60 --- /dev/null +++ b/src/Foundation/RequestDirector.php @@ -0,0 +1,255 @@ +request_builder = $requestBuilder; + $this->response_builder = $responseBuilder; + $this->http = $httpClient; + } + + /** + * @return Response + * @throws BadResponseException + * @throws ClientException + * @throws ServerException + */ + public function Get() + { + $this->request_builder->setMethod(RequestModelInterface::REQUEST_METHOD_GET); + $raw_response = $this->http->Request($this->request_builder->buildRequest()); + $response = $this->response_builder->fromRawResponse($raw_response); + return $response; + } + + /** + * @return Response + * @throws BadResponseException + * @throws ClientException + * @throws ServerException + */ + public function Post() + { + $this->request_builder->setMethod(RequestModelInterface::REQUEST_METHOD_POST); + $raw_response = $this->http->Request($this->request_builder->buildRequest()); + $response = $this->response_builder->fromRawResponse($raw_response); + return $response; + } + + /** + * @return Response + * @throws BadResponseException + * @throws ClientException + * @throws ServerException + */ + public function Patch() + { + $this->request_builder->setMethod(RequestModelInterface::REQUEST_METHOD_PATCH); + $raw_response = $this->http->Request($this->request_builder->buildRequest()); + $response = $this->response_builder->fromRawResponse($raw_response); + return $response; + } + + + /** + * @return Response + * @throws BadResponseException + * @throws ClientException + * @throws ServerException + */ + public function Put() + { + $this->request_builder->setMethod(RequestModelInterface::REQUEST_METHOD_PUT); + $raw_response = $this->http->Request($this->request_builder->buildRequest()); + $response = $this->response_builder->fromRawResponse($raw_response); + return $response; + } + + /** + * @return Response + * @throws BadResponseException + * @throws ClientException + * @throws ServerException + */ + public function Delete() + { + $this->request_builder->setMethod(RequestModelInterface::REQUEST_METHOD_DELETE); + $raw_response = $this->http->Request($this->request_builder->buildRequest()); + $response = $this->response_builder->fromRawResponse($raw_response); + return $response; + } + + /** + * @param array $body + * @return $this + */ + public function JsonBody($body) + { + $this->ContentTypeJson(true); + $this->request_builder->setJsonBody($body); + return $this; + } + + /** + * @param bool $onetime + * @return RequestDirector + */ + public function ContentTypeJson($onetime = false) + { + $this->request_builder->addHeader('content-type', 'application/json', $onetime); + return $this; + } + + /** + * @param string $endpoint + * @return $this + */ + public function Endpoint($endpoint) + { + $this->request_builder->endPoint($endpoint); + return $this; + } + + /** + * @param string $body + * @return $this + */ + public function Body($body) + { + $this->request_builder->setBody($body); + return $this; + } + + /** + * Set Http Client Options + * @param array $options + * @return $this + */ + public function SetHttpOptions(array $options) + { + $this->http->setOptions($options); + return $this; + } + + /** + * @param string $user_agent + * @return $this + */ + public function SetUserAgent($user_agent) + { + $this->http->addOption(CURLOPT_USERAGENT, $user_agent); + return $this; + } + + /** + * @param string $user_name + * @param string $password + * @return $this + */ + public function BasicAuth($user_name, $password) + { + $this->http->addOption(CURLOPT_USERPWD, $user_name . ':' . $password); + return $this; + } + + /** + * @param string $key + * @param mixed $value + * @param bool $onetime + * @return RequestDirector + */ + public function AddQueryString($key, $value, $onetime = false) + { + $this->request_builder->addQueryString($key, $value, $onetime); + return $this; + } + + /** + * @param string $key + * @param bool $onetime + * @return RequestDirector + */ + public function RemoveQueryString($key, $onetime = false) + { + $this->request_builder->removeQueryString($key, $onetime); + return $this; + } + + /** + * @param string $key + * @param mixed $value + * @param bool $onetime + * @return RequestDirector + */ + public function AddHeader($key, $value, $onetime = false) + { + $this->request_builder->addHeader($key, $value, $onetime); + return $this; + } + + /** + * @param string $key + * @param bool $onetime + * @return RequestDirector + */ + public function RemoveHeader($key, $onetime = false) + { + $this->request_builder->removeHeader($key, $onetime); + return $this; + } + + /** + * @param $accept + * @param bool $onetime + * @return RequestDirector + */ + public function AcceptHeader($accept, $onetime = false) + { + $this->request_builder->addHeader('accept', $accept, $onetime); + return $this; + } + + /** + * @param bool $onetime + * @return RequestDirector + */ + public function AcceptJson($onetime = false) + { + $this->request_builder->addHeader('accept', 'application/json', $onetime); + return $this; + } +} diff --git a/src/Foundation/RequestModel.php b/src/Foundation/RequestModel.php new file mode 100644 index 0000000..d97052f --- /dev/null +++ b/src/Foundation/RequestModel.php @@ -0,0 +1,249 @@ +headers; + $headers[$key] = $value; + $this->headers = $headers; + $this->clear_cached_headers(); + return $this; + } + + private function clear_cached_headers() + { + $this->cached_headers = []; + } + + /** + * @param string $key + * @return RequestModel + */ + public function removeHeader($key) + { + $key = strtolower($key); + $headers = (array)$this->headers; + if (isset($headers[$key])) + { + unset($headers[$key]); + $this->headers = $headers; + } + $this->clear_cached_headers(); + return $this; + } + + /** + * @param string $key + * @param string $value + * @return RequestModel + */ + public function addOnetimeHeader($key, $value) + { + $key = strtolower($key); + $headers = (array)$this->onetime_headers; + $headers[$key] = $value; + $this->onetime_headers = $headers; + $this->clear_cached_headers(); + return $this; + } + + /** + * @param string $key + * @return RequestModel + */ + public function removeOnetimeHeader($key) + { + $key = strtolower($key); + $headers = (array)$this->onetime_headers; + if (isset($headers[$key])) + { + unset($headers[$key]); + $this->onetime_headers = $headers; + } + $this->clear_cached_headers(); + return $this; + } + + /** + * @return RequestModel + */ + public function clearOnetimeHeaders() + { + $this->onetime_headers = []; + $this->clear_cached_headers(); + return $this; + } + + /** + * @param string $key + * @param string $value + * @return RequestModel + */ + public function addQueryString($key, $value) + { + $queries = (array)$this->query_string; + $queries[$key] = $value; + $this->query_string = $queries; + return $this; + } + + /** + * @param string $key + * @return RequestModel + */ + public function removeQueryString($key) + { + $queries = (array)$this->query_string; + if (isset($queries[$key])) + { + unset($queries[$key]); + $this->query_string = $queries; + } + return $this; + } + + /** + * @param string $key + * @param string $value + * @return RequestModel + */ + public function addOnetimeQueryString($key, $value) + { + $queries = (array)$this->onetime_query_string; + $queries[$key] = $value; + $this->onetime_query_string = $queries; + return $this; + } + + /** + * @param string $key + * @return RequestModel + */ + public function removeOnetimeQueryString($key) + { + $queries = (array)$this->onetime_query_string; + if (isset($queries[$key])) + { + unset($queries[$key]); + $this->onetime_query_string = $queries; + } + return $this; + } + + /** + * @return RequestModel + */ + public function clearOnetimeQueryStrings() + { + $this->onetime_query_string = []; + return $this; + } + + /** + * @return array + */ + public function getHeaders() + { + if (count($this->cached_headers) > 0) + { + return $this->cached_headers; + } + + $headers = []; + foreach ((array)$this->headers as $key => $header) + { + $headers[] = $this->prepare_header_item($key, $header); + } + foreach ((array)$this->onetime_headers as $key => $header) + { + $headers[] = $this->prepare_header_item($key, $header); + } + $this->set_cached_headers($headers); + return $headers; + } + + /** + * @param $key + * @param $value + * @return string + */ + private function prepare_header_item($key, $value) + { + $key = strtolower($key); + return "{$key}: {$value}"; + } + + private function set_cached_headers(array $headers) + { + $this->cached_headers = $headers; + } + + /** + * @return string + */ + public function getFullUrl() + { + $url = $this->strip_trailing_slash($this->url) . '/' . $this->path; + $query_string = $this->getQueryString(); + if (trim($query_string)) + { + $url = strpos($url, '?') !== false ? $url . '&' . $query_string : $url . '?' . $query_string; + } + return $url; + } + + private function strip_trailing_slash($url) + { + return rtrim($url, '/'); + } + + /** + * @return string + */ + public function getQueryString() + { + return http_build_query(array_merge((array)$this->onetime_query_string, (array)$this->query_string)); + } + + public function getMethod($value) + { + if (trim($value) === '') + { + return RequestModelInterface::REQUEST_METHOD_GET; + } + + return $value; + } +} diff --git a/src/Foundation/ResponseModel.php b/src/Foundation/ResponseModel.php new file mode 100644 index 0000000..b8533b6 --- /dev/null +++ b/src/Foundation/ResponseModel.php @@ -0,0 +1,20 @@ + 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', // RFC2518 + 103 => 'Early Hints', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', // RFC4918 + 208 => 'Already Reported', // RFC5842 + 226 => 'IM Used', // RFC3229 + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', // RFC7238 + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Payload Too Large', + 414 => 'URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Range Not Satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', // RFC2324 + 421 => 'Misdirected Request', // RFC7540 + 422 => 'Unprocessable Entity', // RFC4918 + 423 => 'Locked', // RFC4918 + 424 => 'Failed Dependency', // RFC4918 + 425 => 'Too Early', // RFC-ietf-httpbis-replay-04 + 426 => 'Upgrade Required', // RFC2817 + 428 => 'Precondition Required', // RFC6585 + 429 => 'Too Many Requests', // RFC6585 + 431 => 'Request Header Fields Too Large', // RFC6585 + 451 => 'Unavailable For Legal Reasons', // RFC7725 + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', // RFC2295 + 507 => 'Insufficient Storage', // RFC4918 + 508 => 'Loop Detected', // RFC5842 + 510 => 'Not Extended', // RFC2774 + 511 => 'Network Authentication Required', // RFC6585 + ]; + + public static function httpHeaderFor($code) + { + return 'HTTP/1.1 ' . self::$statusTexts[$code]; + } + + public static function getMessageForCode($code) + { + return self::$statusTexts[$code]; + } + + public static function isError($code) + { + return is_numeric($code) && $code >= self::HTTP_BAD_REQUEST; + } + + public static function canHaveBody($code) + { + return + // True if not in 100s + ($code < self::HTTP_CONTINUE || $code >= self::HTTP_OK) && // and not 204 NO CONTENT + $code != self::HTTP_NO_CONTENT && // and not 304 NOT MODIFIED + $code != self::HTTP_NOT_MODIFIED; + } +} diff --git a/src/HttpClients/Curl.php b/src/HttpClients/Curl.php new file mode 100644 index 0000000..e430384 --- /dev/null +++ b/src/HttpClients/Curl.php @@ -0,0 +1,174 @@ +handler = curl_init(); + } + + /** + * @param Request $request + * @return RawResponse + */ + public function request($request) + { + $options = $this->prepareRequest($request); + + curl_setopt_array($this->handler, $options); + + $_response = curl_exec($this->handler); + $_error_no = curl_errno($this->handler); + $_error = curl_error($this->handler); + $_info = curl_getinfo($this->handler); + + //TODO if debug return this + //var_dump($options, $_info); + + if ($_error_no) + { + throw new RequestException($_error); + } + + $raw_response = new RawResponse([ + 'response' => $_response, + 'info' => $_info, + 'error' => $_error, + ]); + + return $raw_response; + } + + /** + * @param Request $request + * @return array + */ + protected function prepareRequest($request) + { + $_options = (array)$this->getCurlOptions(); + + $options = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CUSTOMREQUEST => strtoupper($request->method), + CURLOPT_HTTPHEADER => $request->getHeaders(), + CURLOPT_HEADER => true, + CURLOPT_URL => $request->getFullUrl(), + + CURLOPT_FOLLOWLOCATION => true, // Follow Redirects If 302 + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_ENCODING => '', + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_CONNECTTIMEOUT => 15, // Time out for a single connection + CURLOPT_TIMEOUT => 30, // Curl Process Timeout + CURLOPT_MAXREDIRS => 10, // Max Redirects Allowed + ]; + + if ( + $request->method === RequestModelInterface::REQUEST_METHOD_POST + || $request->method === RequestModelInterface::REQUEST_METHOD_PUT + || $request->method === RequestModelInterface::REQUEST_METHOD_PATCH + ) + { + $post_fields = $request->body; + if ($request->is_json) + { + $post_fields = json_encode($request->json_body); + } + $options[CURLOPT_POSTFIELDS] = $post_fields; + } + + return $_options + $options; + } + + /** + * @return array + */ + private function getCurlOptions() + { + $options = $this->getOptions(); + + //prevent to change the necessary options + foreach ([ + CURLOPT_RETURNTRANSFER, + CURLOPT_CUSTOMREQUEST, + CURLOPT_HTTPHEADER, + CURLOPT_HEADER, + CURLOPT_URL, + ] as $item) + { + if (isset($options[$item])) + { + unset($options[$item]); + } + } + + return $options; + } + + /** + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * @param array $options + * @return void + */ + public function setOptions(array $options) + { + $this->options = $options; + } + + /** + * @param string $key + * @param mixed $value + * @return void + */ + public function addOption($key, $value) + { + $this->options[$key] = $value; + } + + /** + * @param string $key + * @return void + */ + public function removeOption($key) + { + if (isset($this->options[$key])) + { + unset($this->options[$key]); + } + } + + /** + * @return void + */ + public function clearOptions() + { + $this->options = []; + } +} diff --git a/src/Models/Api.php b/src/Models/Api.php new file mode 100644 index 0000000..d2ca64d --- /dev/null +++ b/src/Models/Api.php @@ -0,0 +1,24 @@ +