Initial commit - clone from lumax/http-component
This commit is contained in:
commit
7dcdb01136
24 changed files with 2351 additions and 0 deletions
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/tests/ export-ignore
|
||||||
|
phpunit.xml export-ignore
|
||||||
|
.gitignore export-ignore
|
||||||
|
.gitattributes export-ignore
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/.idea
|
||||||
|
/coverage
|
||||||
|
/vendor
|
||||||
|
/node_modules
|
||||||
|
/composer.lock
|
||||||
|
/.phpunit.result.cache
|
104
CHANGELOG.md
Normal file
104
CHANGELOG.md
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
# Loom | HTTP Component Changelog
|
||||||
|
|
||||||
|
## [2.3.0] - 2024-07-18
|
||||||
|
### Added
|
||||||
|
- Add `getQueryParam(string $name, ?string $default = null)` method to `Request`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [2.2.1] - 2024-04-30
|
||||||
|
### Added
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix for subsequent calls to `Request::get()` returning null - rewind stream after reading.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [2.2.0] - 2024-04-29
|
||||||
|
### Added
|
||||||
|
- Added `get` method to `Request`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [2.1.0] - 2024-04-17
|
||||||
|
### Added
|
||||||
|
- Added `getData` method to `Request`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [2.0.6] - 2024-03-17
|
||||||
|
### Added
|
||||||
|
- Added `CHANGELOG.md`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- N/A
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- N/A
|
149
README.md
Normal file
149
README.md
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
# Loom | HTTP Component
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<!-- Version Badge -->
|
||||||
|
<img src="https://img.shields.io/badge/Version-1.0.0-blue" alt="Version 1.0.0">
|
||||||
|
<!-- PHP Coverage Badge -->
|
||||||
|
<img src="https://img.shields.io/badge/PHP Coverage-79.23%25-orange" alt="PHP Coverage 79.23%">
|
||||||
|
<!-- License Badge -->
|
||||||
|
<img src="https://img.shields.io/badge/License-GPL--3.0--or--later-34ad9b" alt="License GPL--3.0--or--later">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
The HTTP Component is a PHP library designed to simplify the process of making HTTP requests and handling HTTP responses
|
||||||
|
in your applications. It follows the [PSR-7 HTTP Message Interface](https://www.php-fig.org/psr/psr-7/) standards for
|
||||||
|
HTTP messages, making it compatible with other libraries and frameworks that also adhere to these standards.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
You can install this library using Composer:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require loomlabs/http-component
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
This HTTP Component package provides the following key features:
|
||||||
|
|
||||||
|
### Request and Response Handling
|
||||||
|
|
||||||
|
- `Request` and `Response` classes that implement the `Psr\Http\Message\RequestInterface` and `Psr\Http\Message\ResponseInterface`, respectively.
|
||||||
|
- Easily create and manipulate HTTP requests and responses.
|
||||||
|
- Handle headers, request methods, status codes, and more.
|
||||||
|
|
||||||
|
### Stream Handling
|
||||||
|
|
||||||
|
- A `Stream` class that implements the `Psr\Http\Message\StreamInterface` for working with stream data.
|
||||||
|
- Read and write data to streams, check for stream availability, and more.
|
||||||
|
|
||||||
|
### HTTP Client
|
||||||
|
|
||||||
|
- A `HttpClient` class that implements the `Psr\Http\Client\ClientInterface`.
|
||||||
|
- Simplifies sending HTTP requests using cURL and processing HTTP responses.
|
||||||
|
- Supports common HTTP methods like GET, POST, PUT, PATCH and DELETE.
|
||||||
|
- Automatically parses response headers and handles redirects.
|
||||||
|
|
||||||
|
### URI Handling
|
||||||
|
|
||||||
|
- A `Uri` class that implements the `Psr\Http\Message\UriInterface` for working with URIs.
|
||||||
|
- Easily construct and manipulate URIs, including handling scheme, host, port, path, query, and fragment.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Creating an HTTP Request
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Loom\HttpComponent\HttpClient;
|
||||||
|
use Loom\HttpComponent\Request;
|
||||||
|
use Loom\HttpComponent\StreamBuilder;
|
||||||
|
use Loom\HttpComponent\Uri;
|
||||||
|
|
||||||
|
// Create a URI
|
||||||
|
$uri = new Uri('https', 'example.com', '/');
|
||||||
|
|
||||||
|
// Create an HTTP GET request
|
||||||
|
$body = 'Some text!';
|
||||||
|
$request = new Request(
|
||||||
|
'GET',
|
||||||
|
$uri,
|
||||||
|
['Content-Type' => 'application/json'],
|
||||||
|
StreamBuilder::build($body)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Customise the request headers
|
||||||
|
$request = $request->withHeader('Authorization', 'Bearer AccessToken');
|
||||||
|
|
||||||
|
// Send the request using the built-in HTTP Client
|
||||||
|
$response = (new HttpClient())->sendRequest($request);
|
||||||
|
|
||||||
|
// Get the response status code
|
||||||
|
$status = $response->getStatusCode();
|
||||||
|
|
||||||
|
// Get the response body
|
||||||
|
$body = $response->getBody()->getContents();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating an HTTP Client
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Loom\HttpComponent\HttpClient;
|
||||||
|
|
||||||
|
// Create an HTTP client
|
||||||
|
$client = new HttpClient();
|
||||||
|
|
||||||
|
// Send GET request
|
||||||
|
$response = $client->get('https://example.com/api/resource');
|
||||||
|
|
||||||
|
// Send POST request to endpoint with headers and body
|
||||||
|
$response = $client->post(
|
||||||
|
'https://example.com/api/resource',
|
||||||
|
['Content-Type' => 'application/json', 'Authorization' => 'Bearer AccessToken'],
|
||||||
|
json_encode(['data' => 'value'])
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Working with Streams
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Loom\HttpComponent\StreamBuilder;
|
||||||
|
|
||||||
|
// Create a stream from a string
|
||||||
|
$stream = StreamBuilder::build('Hello, World!');
|
||||||
|
|
||||||
|
// Read from the stream
|
||||||
|
$data = $stream->read(1024);
|
||||||
|
|
||||||
|
// Write to the stream
|
||||||
|
$stream->write('New data to append');
|
||||||
|
|
||||||
|
// Rewind the streams internal pointer
|
||||||
|
$stream->rewind();
|
||||||
|
|
||||||
|
// Get the stream contents
|
||||||
|
$contents = $stream->getContents();
|
||||||
|
```
|
||||||
|
|
||||||
|
### URI Handling
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Loom\HttpComponent\Uri;
|
||||||
|
use Loom\HttpComponent\Web\WebServerUri;
|
||||||
|
|
||||||
|
// Create a URI
|
||||||
|
$uri = new Uri('https', 'example.com', '/api/resource');
|
||||||
|
|
||||||
|
// Modify the URI
|
||||||
|
$uri = $uri->withScheme('http');
|
||||||
|
$uri = $uri->withPort(8888);
|
||||||
|
$uri = $uri->withQuery('new_param=new_value');
|
||||||
|
|
||||||
|
// Get the URI as a string
|
||||||
|
$uriString = $uri->__toString();
|
||||||
|
|
||||||
|
// Build a URI based on the current request to your web server
|
||||||
|
$uri = WebServerUri::generate();
|
||||||
|
```
|
||||||
|
|
||||||
|
### License
|
||||||
|
This package is open-source software licensed under the
|
||||||
|
[GNU General Public License, version 3.0 (GPL-3.0)](https://opensource.org/licenses/GPL-3.0).
|
27
composer.json
Normal file
27
composer.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "loomlabs/http-component",
|
||||||
|
"description": "A component for handling HTTP Requests/Responses",
|
||||||
|
"scripts": {
|
||||||
|
"test": "php -d xdebug.mode=coverage ./vendor/bin/phpunit --testdox --colors=always --coverage-html coverage --coverage-clover coverage/coverage.xml --testdox-html coverage/testdox.html && npx badger --phpunit ./coverage/coverage.xml && npx badger --version ./composer.json && npx badger --license ./composer.json"
|
||||||
|
},
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
|
"require": {
|
||||||
|
"psr/http-message": "^2.0",
|
||||||
|
"psr/http-client": "^1.0",
|
||||||
|
"ext-curl": "*"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Loom\\HttpComponent\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Loom\\HttpComponentTests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^12.1"
|
||||||
|
}
|
||||||
|
}
|
21
package-lock.json
generated
Normal file
21
package-lock.json
generated
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "html",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"devDependencies": {
|
||||||
|
"@dannyxcii/badger": "^0.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dannyxcii/badger": {
|
||||||
|
"version": "0.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dannyxcii/badger/-/badger-0.4.1.tgz",
|
||||||
|
"integrity": "sha512-fK39595AbeKiajI6cxI+Odov+/r8aWXLItNELoxFhf59uzTEI6lL7JUm2t7GyBi0OZieO+7me1zWdRkIONdFOQ==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"badger": "bin/badger"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
package.json
Normal file
13
package.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/DanielWinning/http-component.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/DanielWinning/http-component/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/DanielWinning/http-component#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"@dannyxcii/badger": "^0.4.1"
|
||||||
|
}
|
||||||
|
}
|
22
phpunit.xml
Normal file
22
phpunit.xml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit colors="true"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
displayDetailsOnIncompleteTests="true"
|
||||||
|
displayDetailsOnSkippedTests="true"
|
||||||
|
displayDetailsOnTestsThatTriggerDeprecations="true"
|
||||||
|
displayDetailsOnTestsThatTriggerWarnings="true"
|
||||||
|
displayDetailsOnTestsThatTriggerNotices="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
failOnRisky="true">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit Tests">
|
||||||
|
<directory>tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<coverage/>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">src</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
</phpunit>
|
4
spinner.yaml
Normal file
4
spinner.yaml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
options:
|
||||||
|
environment:
|
||||||
|
database:
|
||||||
|
enabled: false
|
197
src/HttpClient.php
Normal file
197
src/HttpClient.php
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Loom\HttpComponent;
|
||||||
|
|
||||||
|
use Psr\Http\Client\ClientInterface;
|
||||||
|
use Psr\Http\Message\RequestInterface;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\UriInterface;
|
||||||
|
|
||||||
|
class HttpClient implements ClientInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param RequestInterface $request
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function sendRequest(RequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$curl = curl_init();
|
||||||
|
|
||||||
|
$this->setCurlOptions($curl, $request);
|
||||||
|
|
||||||
|
$responseText = $this->getRawResponse($curl);
|
||||||
|
|
||||||
|
$response = (new Response())->withStatus(curl_getinfo($curl, CURLINFO_HTTP_CODE));
|
||||||
|
$headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
|
||||||
|
$response = $this->parseHeaders($response, $responseText, $headerSize);
|
||||||
|
|
||||||
|
return $response->withBody(StreamBuilder::build(substr($responseText, $headerSize)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $endpoint
|
||||||
|
* @param array $headers
|
||||||
|
* @param string $body
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function get(string $endpoint, array $headers = [], string $body = ''): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->buildAndSend('GET', $endpoint, $headers, $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $endpoint
|
||||||
|
* @param array $headers
|
||||||
|
* @param string $body
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function post(string $endpoint, array $headers = [], string $body = ''): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->buildAndSend('POST', $endpoint, $headers, $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $endpoint
|
||||||
|
* @param array $headers
|
||||||
|
* @param string $body
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function put(string $endpoint, array $headers = [], string $body = ''): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->buildAndSend('PUT', $endpoint, $headers, $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $endpoint
|
||||||
|
* @param array $headers
|
||||||
|
* @param string $body
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function patch(string $endpoint, array $headers = [], string $body = ''): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->buildAndSend('PATCH', $endpoint, $headers, $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $endpoint
|
||||||
|
* @param array $headers
|
||||||
|
* @param string $body
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function delete(string $endpoint, array $headers = [], string $body = ''): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->buildAndSend('DELETE', $endpoint, $headers, $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $method
|
||||||
|
* @param string $endpoint
|
||||||
|
* @param array $headers
|
||||||
|
* @param string $body
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
private function buildAndSend(string $method, string $endpoint, array $headers, string $body): ResponseInterface
|
||||||
|
{
|
||||||
|
$request = new Request($method, $this->buildUriFromEndpoint($endpoint), $headers, StreamBuilder::build($body));
|
||||||
|
|
||||||
|
return $this->sendRequest($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $endpoint
|
||||||
|
*
|
||||||
|
* @return UriInterface
|
||||||
|
*/
|
||||||
|
private function buildUriFromEndpoint(string $endpoint): UriInterface
|
||||||
|
{
|
||||||
|
if (!parse_url($endpoint, PHP_URL_SCHEME)) {
|
||||||
|
$endpoint = 'https://' . $endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = parse_url($endpoint);
|
||||||
|
|
||||||
|
return new Uri(
|
||||||
|
$parts['scheme'],
|
||||||
|
$parts['host'] ?? '',
|
||||||
|
$parts['path'] ?? '/',
|
||||||
|
$parts['query'] ?? '',
|
||||||
|
$parts['port'] ?? ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \CurlHandle $curl
|
||||||
|
* @param RequestInterface $request
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function setCurlOptions(\CurlHandle &$curl, RequestInterface $request): void
|
||||||
|
{
|
||||||
|
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $request->getMethod());
|
||||||
|
curl_setopt($curl, CURLOPT_URL, $request->getUri()->__toString());
|
||||||
|
curl_setopt($curl, CURLOPT_HEADER, 1);
|
||||||
|
curl_setopt($curl, CURLOPT_HTTPHEADER, $request->getFlatHeaders());
|
||||||
|
curl_setopt($curl, CURLOPT_POSTFIELDS, $request->getBody()->getContents());
|
||||||
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
|
||||||
|
curl_setopt($curl, CURLINFO_HEADER_OUT, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \CurlHandle $curl
|
||||||
|
*
|
||||||
|
* @return bool|string
|
||||||
|
*
|
||||||
|
* @throws \RuntimeException
|
||||||
|
*/
|
||||||
|
private function getRawResponse(\CurlHandle &$curl): bool|string
|
||||||
|
{
|
||||||
|
$responseText = curl_exec($curl);
|
||||||
|
|
||||||
|
if (curl_errno($curl)) {
|
||||||
|
throw new \RuntimeException(sprintf('cURL error: %s', curl_error($curl)));
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_close($curl);
|
||||||
|
|
||||||
|
return $responseText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ResponseInterface $response
|
||||||
|
* @param string $responseText
|
||||||
|
* @param int $headerSize
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
private function parseHeaders(ResponseInterface &$response, string $responseText, int $headerSize): ResponseInterface
|
||||||
|
{
|
||||||
|
$responseHeaders = substr($responseText, 0, $headerSize);
|
||||||
|
$headerLines = explode("\r\n", trim($responseHeaders));
|
||||||
|
|
||||||
|
foreach ($headerLines as $headerLine) {
|
||||||
|
$splitLine = explode(':', $headerLine, 2);
|
||||||
|
[$key, $value] = count($splitLine) === 2 ? $splitLine : [$splitLine[0], null];
|
||||||
|
|
||||||
|
$key = trim($key);
|
||||||
|
$value = $value ? trim($value) : null;
|
||||||
|
|
||||||
|
if ($key && $value) {
|
||||||
|
if (array_key_exists($key, $response->getHeaders()) && in_array($value, $response->getHeader($key))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $response->withAddedHeader($key, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
298
src/Request.php
Normal file
298
src/Request.php
Normal file
|
@ -0,0 +1,298 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Loom\HttpComponent;
|
||||||
|
|
||||||
|
use Loom\HttpComponent\Traits\ResolveHeadersTrait;
|
||||||
|
use Psr\Http\Message\MessageInterface;
|
||||||
|
use Psr\Http\Message\RequestInterface;
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
use Psr\Http\Message\UriInterface;
|
||||||
|
|
||||||
|
class Request implements RequestInterface
|
||||||
|
{
|
||||||
|
use ResolveHeadersTrait;
|
||||||
|
|
||||||
|
private UriInterface $uri;
|
||||||
|
private StreamInterface $body;
|
||||||
|
private string $method;
|
||||||
|
private array $headers;
|
||||||
|
private string $protocolVersion;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $method,
|
||||||
|
UriInterface $uri,
|
||||||
|
array $headers = [],
|
||||||
|
StreamInterface $body = null,
|
||||||
|
string $protocolVersion = '1.1'
|
||||||
|
) {
|
||||||
|
$this->method = $method;
|
||||||
|
$this->uri = $uri;
|
||||||
|
$this->headers = $this->setHeaders($headers);
|
||||||
|
$this->body = $body ?? new Stream(fopen('php://temp', 'r+'));
|
||||||
|
$this->protocolVersion = $protocolVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getProtocolVersion(): string
|
||||||
|
{
|
||||||
|
return $this->protocolVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $version
|
||||||
|
*
|
||||||
|
* @return MessageInterface
|
||||||
|
*/
|
||||||
|
public function withProtocolVersion(string $version): MessageInterface
|
||||||
|
{
|
||||||
|
$request = clone $this;
|
||||||
|
$request->protocolVersion = $version;
|
||||||
|
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getHeaders(): array
|
||||||
|
{
|
||||||
|
return $this->headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasHeader(string $name): bool
|
||||||
|
{
|
||||||
|
return isset($this->headers[strtolower($name)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
*
|
||||||
|
* @return array|string[]
|
||||||
|
*/
|
||||||
|
public function getHeader(string $name): array
|
||||||
|
{
|
||||||
|
return $this->headers[strtolower($name)] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getHeaderLine(string $name): string
|
||||||
|
{
|
||||||
|
return implode(', ', $this->getHeader($name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @param $value
|
||||||
|
*
|
||||||
|
* @return MessageInterface
|
||||||
|
*/
|
||||||
|
public function withHeader(string $name, $value): MessageInterface
|
||||||
|
{
|
||||||
|
$request = clone $this;
|
||||||
|
$request->headers[strtolower($name)] = is_array($value) ? $value : [$value];
|
||||||
|
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @param $value
|
||||||
|
*
|
||||||
|
* @return MessageInterface
|
||||||
|
*/
|
||||||
|
public function withAddedHeader(string $name, $value): MessageInterface
|
||||||
|
{
|
||||||
|
$name = strtolower($name);
|
||||||
|
$request = clone $this;
|
||||||
|
$request->headers[$name] = array_merge($this->headers[$name] ?? [], is_array($value) ? $value : [$value]);
|
||||||
|
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
*
|
||||||
|
* @return MessageInterface
|
||||||
|
*/
|
||||||
|
public function withoutHeader(string $name): MessageInterface
|
||||||
|
{
|
||||||
|
$request = clone $this;
|
||||||
|
unset($request->headers[strtolower($name)]);
|
||||||
|
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return StreamInterface
|
||||||
|
*/
|
||||||
|
public function getBody(): StreamInterface
|
||||||
|
{
|
||||||
|
return $this->body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param StreamInterface $body
|
||||||
|
*
|
||||||
|
* @return MessageInterface
|
||||||
|
*/
|
||||||
|
public function withBody(StreamInterface $body): MessageInterface
|
||||||
|
{
|
||||||
|
$request = clone $this;
|
||||||
|
$request->body = $body;
|
||||||
|
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getRequestTarget(): string
|
||||||
|
{
|
||||||
|
$path = $this->uri->getPath();
|
||||||
|
$query = $this->uri->getQuery();
|
||||||
|
|
||||||
|
return $path
|
||||||
|
? ($query ? sprintf('%s?%s', $path, $query) : $path)
|
||||||
|
: ($query ? sprintf('/?%s', $query) : '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $requestTarget
|
||||||
|
*
|
||||||
|
* @return RequestInterface
|
||||||
|
*/
|
||||||
|
public function withRequestTarget(string $requestTarget): RequestInterface
|
||||||
|
{
|
||||||
|
$request = clone $this;
|
||||||
|
$request->uri = $request->uri->withPath(strtok($requestTarget, '?'));
|
||||||
|
$request->uri = $request->uri->withQuery(substr(strstr($requestTarget, '?'), 1));
|
||||||
|
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getMethod(): string
|
||||||
|
{
|
||||||
|
return $this->method;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $method
|
||||||
|
*
|
||||||
|
* @return RequestInterface
|
||||||
|
*/
|
||||||
|
public function withMethod(string $method): RequestInterface
|
||||||
|
{
|
||||||
|
$request = clone $this;
|
||||||
|
$request->method = $method;
|
||||||
|
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return UriInterface
|
||||||
|
*/
|
||||||
|
public function getUri(): UriInterface
|
||||||
|
{
|
||||||
|
return $this->uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param UriInterface $uri
|
||||||
|
* @param bool $preserveHost
|
||||||
|
*
|
||||||
|
* @return RequestInterface
|
||||||
|
*/
|
||||||
|
public function withUri(UriInterface $uri, bool $preserveHost = false): RequestInterface
|
||||||
|
{
|
||||||
|
$request = clone $this;
|
||||||
|
$request->uri = $uri;
|
||||||
|
|
||||||
|
if (!$preserveHost) {
|
||||||
|
$request->uri = $request->uri->withHost($request->getHeaderLine('host'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getFlatHeaders(): array
|
||||||
|
{
|
||||||
|
$headerStrings = [];
|
||||||
|
|
||||||
|
foreach ($this->headers as $header => $values) {
|
||||||
|
foreach ($values as $value) {
|
||||||
|
$headerStrings[] = "$header: $value";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $headerStrings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getData(): array
|
||||||
|
{
|
||||||
|
$contentType = $this->getHeaderLine('Content-Type');
|
||||||
|
$bodyContents = $this->getBody()->getContents();
|
||||||
|
|
||||||
|
if ($contentType === 'application/json') {
|
||||||
|
$data = json_decode($bodyContents, true);
|
||||||
|
} else if ($contentType === 'application/x-www-form-urlencoded') {
|
||||||
|
parse_str($bodyContents, $data);
|
||||||
|
} else {
|
||||||
|
$data = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $key
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function get(string $key): mixed
|
||||||
|
{
|
||||||
|
return $this->getData()[$key] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @param string|null $default
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getQueryParam(string $name, ?string $default = null): ?string
|
||||||
|
{
|
||||||
|
return $this->getQueryParams()[$name] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function getQueryParams(): array
|
||||||
|
{
|
||||||
|
$queryParams = [];
|
||||||
|
|
||||||
|
parse_str($this->uri->getQuery(), $queryParams);
|
||||||
|
|
||||||
|
return $queryParams;
|
||||||
|
}
|
||||||
|
}
|
187
src/Response.php
Normal file
187
src/Response.php
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Loom\HttpComponent;
|
||||||
|
|
||||||
|
use Loom\HttpComponent\Traits\ResolveHeadersTrait;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
|
||||||
|
class Response implements ResponseInterface
|
||||||
|
{
|
||||||
|
use ResolveHeadersTrait;
|
||||||
|
|
||||||
|
private int $statusCode;
|
||||||
|
private string $reasonPhrase;
|
||||||
|
private array $headers;
|
||||||
|
private StreamInterface $body;
|
||||||
|
private string $protocolVersion;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
int $statusCode = 200,
|
||||||
|
string $reasonPhrase = '',
|
||||||
|
array $headers = [],
|
||||||
|
StreamInterface $body = null,
|
||||||
|
string $protocolVersion = '1.1'
|
||||||
|
) {
|
||||||
|
$this->statusCode = $statusCode;
|
||||||
|
$this->reasonPhrase = $reasonPhrase;
|
||||||
|
$this->headers = $this->setHeaders($headers);
|
||||||
|
$this->body = $body ?? new Stream(fopen('php://temp', 'r+'));
|
||||||
|
$this->protocolVersion = $protocolVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getProtocolVersion(): string
|
||||||
|
{
|
||||||
|
return $this->protocolVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $version
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function withProtocolVersion(string $version): ResponseInterface
|
||||||
|
{
|
||||||
|
$response = clone $this;
|
||||||
|
$response->protocolVersion = $version;
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getHeaders(): array
|
||||||
|
{
|
||||||
|
return $this->headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasHeader(string $name): bool
|
||||||
|
{
|
||||||
|
return isset($this->headers[strtolower($name)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
*
|
||||||
|
* @return array|string[]
|
||||||
|
*/
|
||||||
|
public function getHeader(string $name): array
|
||||||
|
{
|
||||||
|
return $this->headers[strtolower($name)] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getHeaderLine(string $name): string
|
||||||
|
{
|
||||||
|
return implode(', ', $this->getHeader($name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @param $value
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function withHeader(string $name, $value): ResponseInterface
|
||||||
|
{
|
||||||
|
$response = clone $this;
|
||||||
|
$name = strtolower($name);
|
||||||
|
$response->headers[$name] = is_array($value) ? $value : [$value];
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @param $value
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function withAddedHeader(string $name, $value): ResponseInterface
|
||||||
|
{
|
||||||
|
$response = clone $this;
|
||||||
|
$name = strtolower($name);
|
||||||
|
$response->headers[$name] = array_merge($this->headers[$name] ?? [], is_array($value) ? $value : [$value]);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function withoutHeader(string $name): ResponseInterface
|
||||||
|
{
|
||||||
|
$response = clone $this;
|
||||||
|
$name = strtolower($name);
|
||||||
|
unset($response->headers[$name]);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return StreamInterface
|
||||||
|
*/
|
||||||
|
public function getBody(): StreamInterface
|
||||||
|
{
|
||||||
|
return $this->body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param StreamInterface $body
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function withBody(StreamInterface $body): ResponseInterface
|
||||||
|
{
|
||||||
|
$response = clone $this;
|
||||||
|
$response->body = $body;
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getStatusCode(): int
|
||||||
|
{
|
||||||
|
return $this->statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $code
|
||||||
|
* @param $reasonPhrase
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function withStatus($code, $reasonPhrase = ''): ResponseInterface
|
||||||
|
{
|
||||||
|
$response = clone $this;
|
||||||
|
$response->statusCode = $code;
|
||||||
|
$response->reasonPhrase = $reasonPhrase;
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getReasonPhrase(): string
|
||||||
|
{
|
||||||
|
return $this->reasonPhrase;
|
||||||
|
}
|
||||||
|
}
|
240
src/Stream.php
Normal file
240
src/Stream.php
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Loom\HttpComponent;
|
||||||
|
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
|
||||||
|
class Stream implements StreamInterface
|
||||||
|
{
|
||||||
|
private $resource;
|
||||||
|
|
||||||
|
public function __construct($resource)
|
||||||
|
{
|
||||||
|
if (!is_resource($resource)) {
|
||||||
|
throw new \InvalidArgumentException('Invalid resource provided for Stream');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->resource = $resource;
|
||||||
|
$this->rewind();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->getContents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function close(): void
|
||||||
|
{
|
||||||
|
if (is_resource($this->resource)) {
|
||||||
|
fclose($this->resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return resource
|
||||||
|
*/
|
||||||
|
public function detach(): mixed
|
||||||
|
{
|
||||||
|
$resource = $this->resource;
|
||||||
|
$this->resource = null;
|
||||||
|
|
||||||
|
return $resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int|null
|
||||||
|
*/
|
||||||
|
public function getSize(): ?int
|
||||||
|
{
|
||||||
|
if (!$this->resource) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stats = fstat($this->resource);
|
||||||
|
|
||||||
|
return $stats['size'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function tell(): int
|
||||||
|
{
|
||||||
|
if (!$this->resource) {
|
||||||
|
throw new \RuntimeException('Stream is detached');
|
||||||
|
}
|
||||||
|
|
||||||
|
$position = ftell($this->resource);
|
||||||
|
|
||||||
|
if ($position === false) {
|
||||||
|
throw new \RuntimeException('Unable to get the stream position');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function eof(): bool
|
||||||
|
{
|
||||||
|
if (!$this->resource) {
|
||||||
|
throw new \RuntimeException('Stream is detached');
|
||||||
|
}
|
||||||
|
|
||||||
|
return feof($this->resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isSeekable(): bool
|
||||||
|
{
|
||||||
|
if (!$this->resource) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = stream_get_meta_data($this->resource);
|
||||||
|
|
||||||
|
return isset($meta['seekable']) && $meta['seekable'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $offset
|
||||||
|
* @param int $whence
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function seek(int $offset, int $whence = SEEK_SET): void
|
||||||
|
{
|
||||||
|
if (!$this->isSeekable()) {
|
||||||
|
throw new \RuntimeException('Stream is not seekable');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fseek($this->resource, $offset, $whence) === -1) {
|
||||||
|
throw new \RuntimeException('Unable to seek to stream position ' . $offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function rewind(): void
|
||||||
|
{
|
||||||
|
$this->seek(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isWritable(): bool
|
||||||
|
{
|
||||||
|
if (!$this->resource) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = stream_get_meta_data($this->resource);
|
||||||
|
|
||||||
|
return isset($meta['mode']) && (str_contains($meta['mode'], 'w') || str_contains($meta['mode'], 'a'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $string
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function write(string $string): int
|
||||||
|
{
|
||||||
|
if (!$this->isWritable()) {
|
||||||
|
throw new \RuntimeException('Stream is not writable');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = fwrite($this->resource, $string);
|
||||||
|
|
||||||
|
if ($result === false) {
|
||||||
|
throw new \RuntimeException('Error writing to stream');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isReadable(): bool
|
||||||
|
{
|
||||||
|
if (!$this->resource) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = stream_get_meta_data($this->resource);
|
||||||
|
|
||||||
|
return isset($meta['mode']) && (str_contains($meta['mode'], 'r') || str_contains($meta['mode'], 'a') || str_contains($meta['mode'], '+'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $length
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function read(int $length): string
|
||||||
|
{
|
||||||
|
if (!$this->isReadable()) {
|
||||||
|
throw new \RuntimeException('Stream is not readable');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = fread($this->resource, $length);
|
||||||
|
|
||||||
|
if ($data === false) {
|
||||||
|
throw new \RuntimeException('Error reading from stream');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getContents(): string
|
||||||
|
{
|
||||||
|
if (!$this->isReadable()) {
|
||||||
|
throw new \RuntimeException('Stream is not readable');
|
||||||
|
}
|
||||||
|
|
||||||
|
$contents = stream_get_contents($this->resource);
|
||||||
|
|
||||||
|
if ($contents === false) {
|
||||||
|
throw new \RuntimeException('Error getting stream contents');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->rewind();
|
||||||
|
|
||||||
|
return $contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $key
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getMetadata(mixed $key = null): mixed
|
||||||
|
{
|
||||||
|
if (!$this->resource) {
|
||||||
|
return $key ? null : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = stream_get_meta_data($this->resource);
|
||||||
|
|
||||||
|
if ($key === null) {
|
||||||
|
return $meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $meta[$key] ?? null;
|
||||||
|
}
|
||||||
|
}
|
22
src/StreamBuilder.php
Normal file
22
src/StreamBuilder.php
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Loom\HttpComponent;
|
||||||
|
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
|
||||||
|
class StreamBuilder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string $body
|
||||||
|
*
|
||||||
|
* @return StreamInterface
|
||||||
|
*/
|
||||||
|
public static function build(string $body): StreamInterface
|
||||||
|
{
|
||||||
|
$stream = new Stream(fopen('php://temp', 'r+'));
|
||||||
|
$stream->write($body);
|
||||||
|
$stream->rewind();
|
||||||
|
|
||||||
|
return $stream;
|
||||||
|
}
|
||||||
|
}
|
22
src/Traits/ResolveHeadersTrait.php
Normal file
22
src/Traits/ResolveHeadersTrait.php
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Loom\HttpComponent\Traits;
|
||||||
|
|
||||||
|
trait ResolveHeadersTrait
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array $headers
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function setHeaders(array $headers): array
|
||||||
|
{
|
||||||
|
$sortedHeaders = [];
|
||||||
|
|
||||||
|
foreach ($headers as $key => $header) {
|
||||||
|
$sortedHeaders[strtolower($key)] = is_array($header) ? $header : [$header];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sortedHeaders;
|
||||||
|
}
|
||||||
|
}
|
217
src/Uri.php
Normal file
217
src/Uri.php
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Loom\HttpComponent;
|
||||||
|
|
||||||
|
use Psr\Http\Message\UriInterface;
|
||||||
|
|
||||||
|
class Uri implements UriInterface
|
||||||
|
{
|
||||||
|
private string $scheme;
|
||||||
|
private string $host;
|
||||||
|
private ?string $port;
|
||||||
|
private string $path;
|
||||||
|
private string $query;
|
||||||
|
private string $fragment;
|
||||||
|
private string $userInfo;
|
||||||
|
|
||||||
|
public function __construct(string $scheme, string $host, string $path, string $query, string|int $port = null)
|
||||||
|
{
|
||||||
|
$this->scheme = $scheme;
|
||||||
|
$this->host = $host;
|
||||||
|
$this->port = (string) $port;
|
||||||
|
$this->path = $path;
|
||||||
|
$this->query = $query;
|
||||||
|
$this->fragment = '';
|
||||||
|
$this->userInfo = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getScheme(): string
|
||||||
|
{
|
||||||
|
return $this->scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getAuthority(): string
|
||||||
|
{
|
||||||
|
$authority = $this->host;
|
||||||
|
|
||||||
|
if (!empty($this->userInfo)) {
|
||||||
|
$authority = sprintf('%s@%s', $this->userInfo, $authority);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($this->port)) {
|
||||||
|
$authority = sprintf('%s:%s', $authority, $this->port);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $authority;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getUserInfo(): string
|
||||||
|
{
|
||||||
|
return $this->userInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getHost(): string
|
||||||
|
{
|
||||||
|
return $this->host;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int|null
|
||||||
|
*/
|
||||||
|
public function getPort(): ?int
|
||||||
|
{
|
||||||
|
return (int) $this->port;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getPath(): string
|
||||||
|
{
|
||||||
|
return $this->path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getQuery(): string
|
||||||
|
{
|
||||||
|
return $this->query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getFragment(): string
|
||||||
|
{
|
||||||
|
return $this->fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $scheme
|
||||||
|
*
|
||||||
|
* @return UriInterface
|
||||||
|
*/
|
||||||
|
public function withScheme(string $scheme): UriInterface
|
||||||
|
{
|
||||||
|
$uri = clone $this;
|
||||||
|
$uri->scheme = $scheme;
|
||||||
|
|
||||||
|
return $uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $user
|
||||||
|
* @param string|null $password
|
||||||
|
*
|
||||||
|
* @return UriInterface
|
||||||
|
*/
|
||||||
|
public function withUserInfo(string $user, ?string $password = null): UriInterface
|
||||||
|
{
|
||||||
|
$uri = clone $this;
|
||||||
|
$uri->userInfo = $password ? sprintf('%s:%s', $user, $password) : $user;
|
||||||
|
|
||||||
|
return $uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $host
|
||||||
|
*
|
||||||
|
* @return UriInterface
|
||||||
|
*/
|
||||||
|
public function withHost(string $host): UriInterface
|
||||||
|
{
|
||||||
|
$uri = clone $this;
|
||||||
|
$uri->host = $host;
|
||||||
|
|
||||||
|
return $uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int|null $port
|
||||||
|
*
|
||||||
|
* @return UriInterface
|
||||||
|
*/
|
||||||
|
public function withPort(?int $port): UriInterface
|
||||||
|
{
|
||||||
|
$uri = clone $this;
|
||||||
|
$uri->port = $port;
|
||||||
|
|
||||||
|
return $uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $path
|
||||||
|
*
|
||||||
|
* @return UriInterface
|
||||||
|
*/
|
||||||
|
public function withPath(string $path): UriInterface
|
||||||
|
{
|
||||||
|
$uri = clone $this;
|
||||||
|
$uri->path = $path;
|
||||||
|
|
||||||
|
return $uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $query
|
||||||
|
*
|
||||||
|
* @return UriInterface
|
||||||
|
*/
|
||||||
|
public function withQuery(string $query): UriInterface
|
||||||
|
{
|
||||||
|
$uri = clone $this;
|
||||||
|
$uri->query = $query;
|
||||||
|
|
||||||
|
return $uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $fragment
|
||||||
|
*
|
||||||
|
* @return UriInterface
|
||||||
|
*/
|
||||||
|
public function withFragment(string $fragment): UriInterface
|
||||||
|
{
|
||||||
|
$uri = clone $this;
|
||||||
|
$uri->fragment = $fragment;
|
||||||
|
|
||||||
|
return $uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
$uri = sprintf('%s://%s', $this->scheme, $this->host);
|
||||||
|
|
||||||
|
if (!empty($this->port)) {
|
||||||
|
$uri .= sprintf(':%d', $this->port);
|
||||||
|
}
|
||||||
|
|
||||||
|
$uri .= $this->path;
|
||||||
|
|
||||||
|
if (!empty($this->query)) {
|
||||||
|
$uri .= sprintf('?%s', $this->query);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($this->fragment)) {
|
||||||
|
$uri .= sprintf('#%s', $this->fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $uri;
|
||||||
|
}
|
||||||
|
}
|
26
src/Web/WebServerUri.php
Normal file
26
src/Web/WebServerUri.php
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Loom\HttpComponent\Web;
|
||||||
|
|
||||||
|
use Loom\HttpComponent\Uri;
|
||||||
|
|
||||||
|
class WebServerUri
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return Uri
|
||||||
|
*/
|
||||||
|
public static function generate(): Uri
|
||||||
|
{
|
||||||
|
$scheme = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
||||||
|
$urlParts = parse_url($scheme . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
|
||||||
|
$host = $urlParts['host'];
|
||||||
|
|
||||||
|
return new Uri(
|
||||||
|
$scheme,
|
||||||
|
$host,
|
||||||
|
$urlParts['path'],
|
||||||
|
$urlParts['query'] ?? '',
|
||||||
|
$urlParts['port'] ?? ($_SERVER['SERVER_PORT'] ?? null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
114
tests/HttpClientTest.php
Normal file
114
tests/HttpClientTest.php
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Loom\HttpComponentTests;
|
||||||
|
|
||||||
|
use Loom\HttpComponent\HttpClient;
|
||||||
|
use Loom\HttpComponent\Request;
|
||||||
|
use Loom\HttpComponent\Uri;
|
||||||
|
use Loom\HttpComponentTests\Traits\ProvidesHeaderDataTrait;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class HttpClientTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProvidesHeaderDataTrait;
|
||||||
|
|
||||||
|
private static mixed $serverProcess;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function setUpBeforeClass(): void
|
||||||
|
{
|
||||||
|
self::$serverProcess = proc_open('php -S localhost:8000', [], $pipes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function tearDownAfterClass(): void
|
||||||
|
{
|
||||||
|
proc_terminate(self::$serverProcess);
|
||||||
|
proc_close(self::$serverProcess);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testNotFound(): void
|
||||||
|
{
|
||||||
|
$uri = new Uri('http', 'localhost', '/', '', 8000);
|
||||||
|
$request = new Request('GET', $uri, []);
|
||||||
|
|
||||||
|
$response = (new HttpClient())->sendRequest($request);
|
||||||
|
|
||||||
|
$this->assertEquals(404, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $method
|
||||||
|
*
|
||||||
|
* @dataProvider methodProvider
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testItSendsRequest(string $method): void
|
||||||
|
{
|
||||||
|
$uri = new Uri('http', 'localhost', '/tests/api.php', '', 8000);
|
||||||
|
$request = new Request($method, $uri, []);
|
||||||
|
|
||||||
|
$response = (new HttpClient())->sendRequest($request);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGet(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals(200, (new HttpClient())->get(...$this->getDummyRequestData())->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testPost(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals(200, (new HttpClient())->post(...$this->getDummyRequestData())->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testPut(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals(200, (new HttpClient())->put(...$this->getDummyRequestData())->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testPatch(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals(200, (new HttpClient())->patch(...$this->getDummyRequestData())->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testDelete(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals(200, (new HttpClient())->delete(...$this->getDummyRequestData())->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDummyRequestData(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'http://localhost:8000/tests/api.php',
|
||||||
|
[
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
],
|
||||||
|
''
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
220
tests/RequestTest.php
Normal file
220
tests/RequestTest.php
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Loom\HttpComponentTests;
|
||||||
|
|
||||||
|
use Loom\HttpComponent\Request;
|
||||||
|
use Loom\HttpComponent\Stream;
|
||||||
|
use Loom\HttpComponent\StreamBuilder;
|
||||||
|
use Loom\HttpComponent\Uri;
|
||||||
|
use Loom\HttpComponentTests\Traits\ProvidesHeaderDataTrait;
|
||||||
|
use PHPUnit\Framework\MockObject\Exception as MockObjectException;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class RequestTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProvidesHeaderDataTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws MockObjectException
|
||||||
|
*/
|
||||||
|
public function testGetHeaders(): void
|
||||||
|
{
|
||||||
|
$request = $this->simpleGetRequest();
|
||||||
|
|
||||||
|
$this->assertEquals(['content-type' => ['application/json']], $request->getHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws MockObjectException
|
||||||
|
*/
|
||||||
|
public function testMultipleHeaders(): void
|
||||||
|
{
|
||||||
|
[$uri, $body] = $this->getMockObjects();
|
||||||
|
|
||||||
|
$request = new Request('GET', $uri, ['Content-Type' => ['application/json', 'text/plain']], $body);
|
||||||
|
|
||||||
|
$this->assertEquals(['content-type' => ['application/json', 'text/plain']], $request->getHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws MockObjectException
|
||||||
|
*/
|
||||||
|
public function testWithHeader(): void
|
||||||
|
{
|
||||||
|
$request = $this->simpleGetRequest();
|
||||||
|
|
||||||
|
$request = $request->withHeader('Content-Type', 'text/html');
|
||||||
|
|
||||||
|
$this->assertEquals(['content-type' => ['text/html']], $request->getHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws MockObjectException
|
||||||
|
*/
|
||||||
|
public function testWithAddedHeader(): void
|
||||||
|
{
|
||||||
|
$request = $this->simpleGetRequest();
|
||||||
|
|
||||||
|
$request = $request->withAddedHeader('Content-Type', 'text/html');
|
||||||
|
|
||||||
|
$this->assertEquals(['content-type' => ['application/json', 'text/html']], $request->getHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $key
|
||||||
|
*
|
||||||
|
* @dataProvider headerKeyProvider
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws MockObjectException
|
||||||
|
*/
|
||||||
|
public function testHasHeader(string $key): void
|
||||||
|
{
|
||||||
|
$request = $this->simpleGetRequest();
|
||||||
|
|
||||||
|
$this->assertTrue($request->hasHeader($key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws MockObjectException
|
||||||
|
*/
|
||||||
|
public function testWithoutHeader(): void
|
||||||
|
{
|
||||||
|
$request = $this->simpleGetRequest();
|
||||||
|
|
||||||
|
$request = $request->withoutHeader('CONTENT-TYPE');
|
||||||
|
|
||||||
|
$this->assertEquals([], $request->getHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws MockObjectException
|
||||||
|
*/
|
||||||
|
public function testGetHeaderLine(): void
|
||||||
|
{
|
||||||
|
$request = $this->simpleGetRequest();
|
||||||
|
|
||||||
|
$this->assertEquals('application/json', $request->getHeaderLine('content-type'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $method
|
||||||
|
*
|
||||||
|
* @dataProvider methodProvider
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws MockObjectException
|
||||||
|
*/
|
||||||
|
public function testWithMethod(string $method): void
|
||||||
|
{
|
||||||
|
$request = $this->simpleGetRequest();
|
||||||
|
|
||||||
|
$request = $request->withMethod($method);
|
||||||
|
|
||||||
|
$this->assertEquals($method, $request->getMethod());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetRequestTarget(): void
|
||||||
|
{
|
||||||
|
$uri = new Uri('https', 'localhost', '/test', 'hello=world');
|
||||||
|
$body = StreamBuilder::build('test');
|
||||||
|
$request = new Request('GET', $uri, ['Content-Type' => 'application/json'], $body);
|
||||||
|
|
||||||
|
$this->assertEquals('/test?hello=world', $request->getRequestTarget());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWithRequestTarget(): void
|
||||||
|
{
|
||||||
|
$uri = new Uri('https', 'localhost', '/test', 'hello=world');
|
||||||
|
$body = StreamBuilder::build('test');
|
||||||
|
$request = new Request('GET', $uri, ['Content-Type' => 'application/json'], $body);
|
||||||
|
|
||||||
|
$request = $request->withRequestTarget('/default?var=1');
|
||||||
|
|
||||||
|
$this->assertEquals('/default?var=1', $request->getRequestTarget());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws MockObjectException
|
||||||
|
*/
|
||||||
|
public function testWithUri(): void
|
||||||
|
{
|
||||||
|
$request = $this->simpleGetRequest();
|
||||||
|
$uri = new Uri('https', 'localhost', '/test', 'hello=world');
|
||||||
|
|
||||||
|
$request = $request->withUri($uri, true);
|
||||||
|
|
||||||
|
$this->assertSame($uri, $request->getUri());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @throws MockObjectException
|
||||||
|
*/
|
||||||
|
public function testGetData(): void
|
||||||
|
{
|
||||||
|
// application/json
|
||||||
|
$uri = $this->createMock(Uri::class);
|
||||||
|
$body = StreamBuilder::build('{"key":"value"}');
|
||||||
|
$request = new Request('GET', $uri, ['Content-Type' => 'application/json'], $body);
|
||||||
|
$this->assertEquals(['key' => 'value'], $request->getData());
|
||||||
|
|
||||||
|
// application/x-www-form-urlencoded
|
||||||
|
$body = StreamBuilder::build('key=value');
|
||||||
|
$request = new Request('GET', $uri, ['Content-Type' => 'application/x-www-form-urlencoded'], $body);
|
||||||
|
$this->assertEquals(['key' => 'value'], $request->getData());
|
||||||
|
|
||||||
|
// other
|
||||||
|
$body = StreamBuilder::build('key=value');
|
||||||
|
$request = new Request('GET', $uri, ['Content-Type' => 'text/plain'], $body);
|
||||||
|
$this->assertEquals([], $request->getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*
|
||||||
|
* @throws MockObjectException
|
||||||
|
*/
|
||||||
|
private function getMockObjects(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
$this->createMock(Uri::class),
|
||||||
|
$this->createMock(Stream::class),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Request
|
||||||
|
*
|
||||||
|
* @throws MockObjectException
|
||||||
|
*/
|
||||||
|
private function simpleGetRequest(): Request
|
||||||
|
{
|
||||||
|
[$uri, $body] = $this->getMockObjects();
|
||||||
|
|
||||||
|
return new Request('GET', $uri, ['Content-Type' => 'application/json'], $body);
|
||||||
|
}
|
||||||
|
}
|
119
tests/ResponseTest.php
Normal file
119
tests/ResponseTest.php
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Loom\HttpComponentTests;
|
||||||
|
|
||||||
|
use Loom\HttpComponent\Response;
|
||||||
|
use Loom\HttpComponentTests\Traits\ProvidesHeaderDataTrait;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ResponseTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProvidesHeaderDataTrait;
|
||||||
|
|
||||||
|
public function testGetProtocolVersion(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('1.1', (new Response())->getProtocolVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWithProtocolVersion(): void
|
||||||
|
{
|
||||||
|
$response = new Response();
|
||||||
|
$response = $response->withProtocolVersion('2.0');
|
||||||
|
|
||||||
|
$this->assertEquals('2.0', $response->getProtocolVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetHeaders(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals(['content-type' => ['application/json']], ($this->getSimpleResponse())->getHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $key
|
||||||
|
*
|
||||||
|
* @dataProvider headerKeyProvider
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testHasHeader(string $key): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(($this->getSimpleResponse())->hasHeader($key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetHeader(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals(['application/json'], ($this->getSimpleResponse())->getHeader('CONTENT-TYPE'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWithHeader(): void
|
||||||
|
{
|
||||||
|
$response = $this->getSimpleResponse();
|
||||||
|
$response = $response->withHeader('content-type', 'text/html');
|
||||||
|
|
||||||
|
$this->assertEquals(['text/html'], $response->getHeader('content-type'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWithAddedHeader(): void
|
||||||
|
{
|
||||||
|
$response = $this->getSimpleResponse();
|
||||||
|
$response = $response->withAddedHeader('content-type', 'text/html');
|
||||||
|
|
||||||
|
$this->assertEquals(['application/json', 'text/html'], $response->getHeader('content-type'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetHeaderLine(): void
|
||||||
|
{
|
||||||
|
$response = $this->getSimpleResponse();
|
||||||
|
$response = $response->withAddedHeader('content-type', 'text/html');
|
||||||
|
|
||||||
|
$this->assertEquals('application/json, text/html', $response->getHeaderLine('content-type'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWithoutHeader(): void
|
||||||
|
{
|
||||||
|
$response = $this->getSimpleResponse();
|
||||||
|
$response = $response->withoutHeader('CONTENT-TYPE');
|
||||||
|
|
||||||
|
$this->assertEquals([], $response->getHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWithStatus(): void
|
||||||
|
{
|
||||||
|
$response = $this->getSimpleResponse();
|
||||||
|
$response = $response->withStatus(404);
|
||||||
|
|
||||||
|
$this->assertEquals(404, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
private function getSimpleResponse(): Response
|
||||||
|
{
|
||||||
|
return new Response(200, 'OK', ['Content-Type' => 'application/json']);
|
||||||
|
}
|
||||||
|
}
|
182
tests/StreamTest.php
Normal file
182
tests/StreamTest.php
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Loom\HttpComponentTests;
|
||||||
|
|
||||||
|
use Loom\HttpComponent\Stream;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class StreamTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testToString(): void
|
||||||
|
{
|
||||||
|
[$stream, $data] = $this->getWritableStreamWithData();
|
||||||
|
|
||||||
|
$this->assertEquals($data, $stream->__toString());
|
||||||
|
|
||||||
|
$stream->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetSize(): void
|
||||||
|
{
|
||||||
|
[$stream, $data] = $this->getWritableStreamWithData();
|
||||||
|
|
||||||
|
$this->assertEquals(strlen($data), $stream->getSize());
|
||||||
|
|
||||||
|
$stream->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testTell(): void
|
||||||
|
{
|
||||||
|
[$stream] = $this->getWritableStreamWithData();
|
||||||
|
|
||||||
|
$this->assertEquals(0, $stream->tell());
|
||||||
|
$stream->seek(5);
|
||||||
|
$this->assertEquals(5, $stream->tell());
|
||||||
|
|
||||||
|
$stream->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testClose(): void
|
||||||
|
{
|
||||||
|
$resource = fopen('php://temp', 'r+');
|
||||||
|
$stream = new Stream($resource);
|
||||||
|
|
||||||
|
$this->assertTrue(is_resource($resource));
|
||||||
|
|
||||||
|
$stream->close();
|
||||||
|
|
||||||
|
$this->assertFalse(is_resource($resource));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testDetach(): void
|
||||||
|
{
|
||||||
|
$resource = fopen('php://temp', 'r+');
|
||||||
|
$stream = new Stream($resource);
|
||||||
|
|
||||||
|
$detachedResource = $stream->detach();
|
||||||
|
|
||||||
|
$this->assertSame($resource, $detachedResource);
|
||||||
|
$this->assertNull($stream->detach());
|
||||||
|
|
||||||
|
$stream->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testEof(): void
|
||||||
|
{
|
||||||
|
[$stream, $data] = $this->getWritableStreamWithData();
|
||||||
|
|
||||||
|
$this->assertFalse($stream->eof());
|
||||||
|
|
||||||
|
$stream->read(strlen($data) + 1);
|
||||||
|
|
||||||
|
$this->assertTrue($stream->eof());
|
||||||
|
|
||||||
|
$stream->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testIsSeekableWithSeekableResource(): void
|
||||||
|
{
|
||||||
|
[$stream] = $this->getWritableStreamWithData();
|
||||||
|
|
||||||
|
$this->assertTrue($stream->isSeekable());
|
||||||
|
|
||||||
|
$stream->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testSeek(): void
|
||||||
|
{
|
||||||
|
[$stream] = $this->getWritableStreamWithData();
|
||||||
|
|
||||||
|
$stream->seek(5);
|
||||||
|
|
||||||
|
$this->assertEquals('string', $stream->getContents());
|
||||||
|
|
||||||
|
$stream->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testRead(): void
|
||||||
|
{
|
||||||
|
[$stream] = $this->getWritableStreamWithData();
|
||||||
|
|
||||||
|
$read = [];
|
||||||
|
|
||||||
|
$read[] = $stream->read(4);
|
||||||
|
$read[] = $stream->read(7);
|
||||||
|
$read[] = $stream->read(12);
|
||||||
|
|
||||||
|
$this->assertSame('Test', $read[0]);
|
||||||
|
$this->assertSame(' string', $read[1]);
|
||||||
|
$this->assertSame('', $read[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWrite(): void
|
||||||
|
{
|
||||||
|
$resource = fopen('php://temp', 'r+');
|
||||||
|
$stream = new Stream($resource);
|
||||||
|
|
||||||
|
$writeData = 'Some text';
|
||||||
|
$stream->write($writeData);
|
||||||
|
|
||||||
|
$stream->rewind();
|
||||||
|
|
||||||
|
$this->assertSame(strlen($writeData), $stream->getSize());
|
||||||
|
$this->assertSame($writeData, $stream->getContents());
|
||||||
|
|
||||||
|
$stream->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetMetadata(): void
|
||||||
|
{
|
||||||
|
[$stream] = $this->getWritableStreamWithData();
|
||||||
|
|
||||||
|
$this->assertNotNull($stream->getMetadata('wrapper_type'));
|
||||||
|
|
||||||
|
$stream->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function getWritableStreamWithData(): array
|
||||||
|
{
|
||||||
|
$data = 'Test string';
|
||||||
|
$resource = fopen('php://temp', 'r+');
|
||||||
|
fwrite($resource, 'Test string');
|
||||||
|
$stream = new Stream($resource);
|
||||||
|
|
||||||
|
return [$stream, $data];
|
||||||
|
}
|
||||||
|
}
|
45
tests/Traits/ProvidesHeaderDataTrait.php
Normal file
45
tests/Traits/ProvidesHeaderDataTrait.php
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Loom\HttpComponentTests\Traits;
|
||||||
|
|
||||||
|
trait ProvidesHeaderDataTrait
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function methodProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'POST' => [
|
||||||
|
'POST',
|
||||||
|
],
|
||||||
|
'PUT' => [
|
||||||
|
'PUT',
|
||||||
|
],
|
||||||
|
'DELETE' => [
|
||||||
|
'DELETE',
|
||||||
|
],
|
||||||
|
'GET' => [
|
||||||
|
'GET',
|
||||||
|
],
|
||||||
|
'PATCH' => [
|
||||||
|
'PATCH',
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function headerKeyProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Content-Type' => [
|
||||||
|
'key' => 'Content-Type',
|
||||||
|
],
|
||||||
|
'content-type' => [
|
||||||
|
'key' => 'content-type',
|
||||||
|
],
|
||||||
|
'CONTENT-TYPE' => [
|
||||||
|
'key' => 'CONTENT-TYPE',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
109
tests/UriTest.php
Normal file
109
tests/UriTest.php
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Loom\HttpComponentTests;
|
||||||
|
|
||||||
|
use Loom\HttpComponent\Uri;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Http\Message\UriInterface;
|
||||||
|
|
||||||
|
class UriTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetScheme(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('https', ($this->getDefaultUri())->getScheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testGetAuthority(): void
|
||||||
|
{
|
||||||
|
$uri = new Uri('https', 'test.com', '/path', 'var=1', 443);
|
||||||
|
$this->assertEquals('test.com:443', $uri->getAuthority());
|
||||||
|
|
||||||
|
$uri = new Uri('ftp', 'user:password@test.com', '/path', 'var=1');
|
||||||
|
$this->assertEquals('user:password@test.com', $uri->getAuthority());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWithPath(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('/', ($this->getDefaultUri())->getPath());
|
||||||
|
$this->assertEquals('/new', ($this->getDefaultUri())->withPath('/new')->getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testToString(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('https://test.com/', ($this->getDefaultUri())->__toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWithUserInfo(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
'username:password',
|
||||||
|
($this->getDefaultUri())->withUserInfo('username', 'password')->getUserInfo()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWithScheme(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('ftp', ($this->getDefaultUri())->withScheme('ftp')->getScheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWithHost(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('localhost', ($this->getDefaultUri())->withHost('localhost')->getHost());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWithPort(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals(8080, ($this->getDefaultUri())->withPort(8080)->getPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWithQuery(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
'name=test&hello=world',
|
||||||
|
($this->getDefaultUri())->withQuery('name=test&hello=world')->getQuery()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWithFragment(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals('news', ($this->getDefaultUri())->withFragment('news')->getFragment());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return UriInterface
|
||||||
|
*/
|
||||||
|
private function getDefaultUri(): UriInterface
|
||||||
|
{
|
||||||
|
return new Uri('https', 'test.com', '/', '');
|
||||||
|
}
|
||||||
|
}
|
3
tests/api.php
Normal file
3
tests/api.php
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
echo 'Test API endpoint';
|
Loading…
Add table
Reference in a new issue