userAgent = str_replace([ '{curl}', '{php}', ], [ curl_version()['version'] ?? "???", phpversion() ], $this->userAgent); } public function post(string $url, mixed $data, array $headers = [], array $curlOptions = []) : mixed { return $this->request(MethodEnum::Post, $url, $data, $headers, $curlOptions); } public function get(string $url, mixed $data, array $headers = [], array $curlOptions = []) : mixed { return $this->request(MethodEnum::Get, $url, $data, $headers, $curlOptions); } public function delete(string $url, mixed $data, array $headers = [], array $curlOptions = []) : mixed { return $this->request(MethodEnum::Delete, $url, $data, $headers, $curlOptions); } public function patch(string $url, mixed $data, array $headers = [], array $curlOptions = []) : mixed { return $this->request(MethodEnum::Patch, $url, $data, $headers, $curlOptions); } public function put(string $url, mixed $data, array $headers = [], array $curlOptions = []) : mixed { return $this->request(MethodEnum::Put, $url, $data, $headers, $curlOptions); } public function fromRequest(RequestInterface $request) : ResponseInterface { return $this->request(MethodEnum::from($request->getMethod()), $request->getUri(), $request->getBody(), $request->getHeaders()); } public function request(MethodEnum $method, string $url, mixed $data = null, array $headers = [], array $options = []) : ResponseInterface { $response = new Response(); if ($this->debug) { $this->stderr = Stream::fromMemory(); $stderrResource = $this->stderr->detach(); } $headers = array_merge_recursive(HttpHeaderEnum::normalizeHeaderArray($headers), HttpHeaderEnum::normalizeHeaderArray($this->headers)); $options += $this->curlOptions; $this->applyMethod($method, $data, $headers, $options); $headers += $this->authorization->headers; $options += $this->authorization->options; $options += [ CURLOPT_URL => $url, CURLOPT_HTTPHEADER => HttpHeaderEnum::compileHeaders($headers), CURLOPT_MAXREDIRS => $this->maximumRedirections, CURLOPT_TIMEOUT => $this->timeout * 1000, CURLOPT_CONNECTTIMEOUT => $this->connectTimeout, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_HEADERFUNCTION => function($curl, $headerLine) use (& $response) { if ( strpos($headerLine, ':') ) { list($key, $value) = array_map('trim', explode(':', $headerLine)); $response = $response->withHeader($key, $value); } elseif ( strtoupper(substr($headerLine, 0, 4)) === 'HTTP' ) { list(,$code, $status) = explode(' ', trim($headerLine), 3) + [ null, null, null ]; $response = $response->withStatus($code, $status); } return strlen($headerLine); } ] + ( $this->debug ? [ CURLOPT_VERBOSE => true, CURLOPT_STDERR => $stderrResource, ] : [] ); $ch = curl_init(); foreach($options as $opt => $value) { curl_setopt($ch, $opt, $value); } if ( false === ( $result = curl_exec($ch) ) ) { $errno = curl_errno($ch); $errors = array_filter([ curl_error($ch) , CurlErrors::CURL_CODES[$errno] ?? null ]); throw new \Exception(implode(PHP_EOL, $errors), $errno); } if ($this->debug) { $this->stderr->attach($stderrResource); } $response = $response->withBody( Stream::fromMemory($result) ); if ( $response->hasHeader('Content-Type') && false !== strpos($response->getHeader('Content-Type')[0], 'json') ) { $response = JsonResponse::fromResponse($response); } return $response; } protected function applyMethod(MethodEnum $method, mixed &$data, array &$headers, array &$options) : void { switch ($method) { case MethodEnum::Patch: case MethodEnum::Put: case MethodEnum::Delete: case MethodEnum::Post: if ($method === MethodEnum::Post) { $options[CURLOPT_POST] = true; } else { $options[CURLOPT_CUSTOMREQUEST] = $method->value; } $this->arrayToPostFields($data, $headers, $options); break; case MethodEnum::Get: default: } } protected function arrayToPostFields(mixed &$data, array &$headers, array &$options) : void { $headers["Content-Type"] = $this->contentType->value; switch($this->contentType) { case ContentTypeEnum::Json: if ($data instanceof JsonStream) { $json = $data->getContents(); } elseif (is_string($data) && empty($data)) { $json = $this->contentType->defaultBody(); } else { $json = json_encode($data, JsonStream::DEFAULT_JSON_ENCODING_FLAGS); } $options[CURLOPT_POSTFIELDS] = $json; $headers['Accept'] ??= $this->contentType->value; break; case ContentTypeEnum::UrlEncoded: $options[CURLOPT_POSTFIELDS] = http_build_query($data, '', '&'); break; case ContentTypeEnum::Multipart: $options[CURLOPT_POSTFIELDS] = $data; break; } } protected function unindexQueryStringArrays(string $query) : string { return preg_replace("/\[[0-9]+\]/simU", "[]", $query); } }