Skip to content

Commit 8976088

Browse files
authored
Merge pull request #16 from utopia-php/feat-adapters
2 parents a96a010 + 37ca2e7 commit 8976088

21 files changed

Lines changed: 1685 additions & 129 deletions

.github/workflows/tests.yml

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,35 @@
11
name: "Tests"
22

3-
on: [ pull_request ]
3+
on: [pull_request]
44
jobs:
5-
lint:
5+
tests:
66
name: Tests
77
runs-on: ubuntu-latest
88

99
steps:
1010
- name: Checkout repository
11-
uses: actions/checkout@v3
11+
uses: actions/checkout@v4
1212
with:
1313
fetch-depth: 2
1414

1515
- run: git checkout HEAD^2
16-
17-
- name: Install dependencies
18-
run: composer install --profile --ignore-platform-reqs
19-
20-
- name: Run Tests
21-
run: php -S localhost:8000 tests/router.php &
22-
composer test
2316

17+
- name: Set up Docker Buildx
18+
uses: docker/setup-buildx-action@v3
19+
20+
- name: Build image
21+
uses: docker/build-push-action@v6
22+
with:
23+
context: .
24+
push: false
25+
tags: fetch-dev
26+
load: true
27+
cache-from: type=gha
28+
cache-to: type=gha,mode=max
29+
30+
- name: Start Server
31+
run: |
32+
docker compose up -d --wait --wait-timeout 30
33+
34+
- name: Run Tests
35+
run: docker compose exec -T php vendor/bin/phpunit --configuration phpunit.xml

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ vendor
22
*.cache
33
composer.lock
44
state.json
5+
.idea

Dockerfile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
FROM composer:2.0 AS step0
2+
3+
WORKDIR /src/
4+
5+
COPY ./composer.json /src/
6+
7+
RUN composer update --ignore-platform-reqs --optimize-autoloader \
8+
--no-plugins --no-scripts --prefer-dist
9+
10+
FROM appwrite/utopia-base:php-8.4-0.2.1 AS final
11+
12+
LABEL maintainer="team@utopia.io"
13+
14+
WORKDIR /code
15+
16+
COPY --from=step0 /src/vendor /code/vendor
17+
18+
# Add Source Code
19+
COPY ./src /code/src
20+
COPY ./tests /code/tests
21+
COPY ./phpunit.xml /code/
22+
23+
EXPOSE 8000
24+
25+
CMD [ "php", "-S", "0.0.0.0:8000", "tests/router.php"]

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"require-dev": {
1010
"phpstan/phpstan": "^1.10",
1111
"phpunit/phpunit": "^9.5",
12-
"laravel/pint": "^1.5.0"
12+
"laravel/pint": "^1.5.0",
13+
"swoole/ide-helper": "^6.0"
1314
},
1415
"scripts": {
1516
"lint": "./vendor/bin/pint --test --config pint.json",
@@ -23,4 +24,4 @@
2324
}
2425
},
2526
"authors": []
26-
}
27+
}

docker-compose.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
services:
2+
php:
3+
image: fetch-dev
4+
build:
5+
context: .
6+
ports:
7+
- 8000:8000
8+
volumes:
9+
- ./tests:/code/tests
10+
- ./src:/code/src

phpstan.neon

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
parameters:
2-
level: 8
2+
level: max
33
paths:
44
- src
5-
- tests
5+
- tests
6+
scanFiles:
7+
- vendor/swoole/ide-helper/src/swoole_library/src/core/Coroutine/functions.php
8+
scanDirectories:
9+
- vendor/swoole/ide-helper/src/swoole

src/Adapter.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Utopia\Fetch;
6+
7+
use Utopia\Fetch\Options\Request as RequestOptions;
8+
9+
/**
10+
* Adapter interface
11+
* Defines the contract for HTTP adapters
12+
* @package Utopia\Fetch
13+
*/
14+
interface Adapter
15+
{
16+
/**
17+
* Send an HTTP request
18+
*
19+
* @param string $url The URL to send the request to
20+
* @param string $method The HTTP method (GET, POST, etc.)
21+
* @param mixed $body The request body (string, array, or null)
22+
* @param array<string, string> $headers The request headers (formatted as key-value pairs)
23+
* @param RequestOptions $options Request options (timeout, connectTimeout, maxRedirects, allowRedirects, userAgent)
24+
* @param callable|null $chunkCallback Optional callback for streaming chunks
25+
* @return Response The HTTP response
26+
* @throws Exception If the request fails
27+
*/
28+
public function send(
29+
string $url,
30+
string $method,
31+
mixed $body,
32+
array $headers,
33+
RequestOptions $options,
34+
?callable $chunkCallback = null
35+
): Response;
36+
}

src/Adapter/Curl.php

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Utopia\Fetch\Adapter;
6+
7+
use CurlHandle;
8+
use Utopia\Fetch\Adapter;
9+
use Utopia\Fetch\Chunk;
10+
use Utopia\Fetch\Exception;
11+
use Utopia\Fetch\Options\Curl as CurlOptions;
12+
use Utopia\Fetch\Options\Request as RequestOptions;
13+
use Utopia\Fetch\Response;
14+
15+
/**
16+
* Curl Adapter
17+
* HTTP adapter using PHP's cURL extension
18+
* @package Utopia\Fetch\Adapter
19+
*/
20+
class Curl implements Adapter
21+
{
22+
private ?CurlHandle $handle = null;
23+
24+
/**
25+
* @var array<int, mixed>
26+
*/
27+
private array $config = [];
28+
29+
/**
30+
* Create a new Curl adapter
31+
*
32+
* @param CurlOptions|null $options Curl adapter options
33+
*/
34+
public function __construct(?CurlOptions $options = null)
35+
{
36+
$options ??= new CurlOptions();
37+
38+
$this->config[CURLOPT_SSL_VERIFYPEER] = $options->getSslVerifyPeer();
39+
$this->config[CURLOPT_SSL_VERIFYHOST] = $options->getSslVerifyHost() ? 2 : 0;
40+
41+
if ($options->getSslCertificate() !== null) {
42+
$this->config[CURLOPT_SSLCERT] = $options->getSslCertificate();
43+
}
44+
45+
if ($options->getSslKey() !== null) {
46+
$this->config[CURLOPT_SSLKEY] = $options->getSslKey();
47+
}
48+
49+
if ($options->getCaInfo() !== null) {
50+
$this->config[CURLOPT_CAINFO] = $options->getCaInfo();
51+
}
52+
53+
if ($options->getCaPath() !== null) {
54+
$this->config[CURLOPT_CAPATH] = $options->getCaPath();
55+
}
56+
57+
if ($options->getProxy() !== null) {
58+
$this->config[CURLOPT_PROXY] = $options->getProxy();
59+
$this->config[CURLOPT_PROXYTYPE] = $options->getProxyType();
60+
61+
if ($options->getProxyUserPwd() !== null) {
62+
$this->config[CURLOPT_PROXYUSERPWD] = $options->getProxyUserPwd();
63+
}
64+
}
65+
66+
$this->config[CURLOPT_HTTP_VERSION] = $options->getHttpVersion();
67+
$this->config[CURLOPT_TCP_KEEPALIVE] = $options->getTcpKeepAlive() ? 1 : 0;
68+
$this->config[CURLOPT_TCP_KEEPIDLE] = $options->getTcpKeepIdle();
69+
$this->config[CURLOPT_TCP_KEEPINTVL] = $options->getTcpKeepInterval();
70+
$this->config[CURLOPT_BUFFERSIZE] = $options->getBufferSize();
71+
$this->config[CURLOPT_VERBOSE] = $options->getVerbose();
72+
}
73+
74+
/**
75+
* Get or create the cURL handle
76+
*
77+
* @return CurlHandle
78+
* @throws Exception If cURL initialization fails
79+
*/
80+
private function getHandle(): CurlHandle
81+
{
82+
if ($this->handle === null) {
83+
$handle = curl_init();
84+
if ($handle === false) {
85+
throw new Exception('Failed to initialize cURL handle');
86+
}
87+
$this->handle = $handle;
88+
} else {
89+
curl_reset($this->handle);
90+
}
91+
92+
return $this->handle;
93+
}
94+
95+
/**
96+
* Send an HTTP request using cURL
97+
*
98+
* @param string $url The URL to send the request to
99+
* @param string $method The HTTP method (GET, POST, etc.)
100+
* @param mixed $body The request body (string, array, or null)
101+
* @param array<string, string> $headers The request headers (formatted as key-value pairs)
102+
* @param RequestOptions $options Request options (timeout, connectTimeout, maxRedirects, allowRedirects, userAgent)
103+
* @param callable|null $chunkCallback Optional callback for streaming chunks
104+
* @return Response The HTTP response
105+
* @throws Exception If the request fails
106+
*/
107+
public function send(
108+
string $url,
109+
string $method,
110+
mixed $body,
111+
array $headers,
112+
RequestOptions $options,
113+
?callable $chunkCallback = null
114+
): Response {
115+
$formattedHeaders = array_map(function ($key, $value) {
116+
return $key . ':' . $value;
117+
}, array_keys($headers), $headers);
118+
119+
$responseHeaders = [];
120+
$responseBody = '';
121+
$chunkIndex = 0;
122+
123+
$ch = $this->getHandle();
124+
$curlOptions = [
125+
CURLOPT_URL => $url,
126+
CURLOPT_HTTPHEADER => $formattedHeaders,
127+
CURLOPT_CUSTOMREQUEST => $method,
128+
CURLOPT_HEADERFUNCTION => function ($curl, $header) use (&$responseHeaders) {
129+
$len = strlen($header);
130+
$header = explode(':', $header, 2);
131+
if (count($header) < 2) {
132+
return $len;
133+
}
134+
$responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
135+
return $len;
136+
},
137+
CURLOPT_WRITEFUNCTION => function ($ch, $data) use ($chunkCallback, &$responseBody, &$chunkIndex) {
138+
if ($chunkCallback !== null) {
139+
$chunk = new Chunk(
140+
data: $data,
141+
size: strlen($data),
142+
timestamp: microtime(true),
143+
index: $chunkIndex++
144+
);
145+
$chunkCallback($chunk);
146+
} else {
147+
$responseBody .= $data;
148+
}
149+
return strlen($data);
150+
},
151+
CURLOPT_CONNECTTIMEOUT_MS => $options->getConnectTimeout(),
152+
CURLOPT_TIMEOUT_MS => $options->getTimeout(),
153+
CURLOPT_MAXREDIRS => $options->getMaxRedirects(),
154+
CURLOPT_FOLLOWLOCATION => $options->getAllowRedirects(),
155+
CURLOPT_USERAGENT => $options->getUserAgent()
156+
];
157+
158+
if ($body !== null && $body !== [] && $body !== '') {
159+
$curlOptions[CURLOPT_POSTFIELDS] = $body;
160+
}
161+
162+
// Merge adapter config (adapter config takes precedence)
163+
$curlOptions = $this->config + $curlOptions;
164+
165+
foreach ($curlOptions as $option => $value) {
166+
curl_setopt($ch, $option, $value);
167+
}
168+
169+
$success = curl_exec($ch);
170+
if ($success === false) {
171+
$errorMsg = curl_error($ch);
172+
throw new Exception($errorMsg);
173+
}
174+
175+
$responseStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
176+
177+
return new Response(
178+
statusCode: $responseStatusCode,
179+
headers: $responseHeaders,
180+
body: $responseBody
181+
);
182+
}
183+
184+
/**
185+
* Close the cURL handle when the adapter is destroyed
186+
*/
187+
public function __destruct()
188+
{
189+
if ($this->handle !== null) {
190+
curl_close($this->handle);
191+
$this->handle = null;
192+
}
193+
}
194+
}

0 commit comments

Comments
 (0)