Skip to content

Commit 7935a54

Browse files
committed
feat: adds flexible request data
1 parent f2552c5 commit 7935a54

File tree

3 files changed

+177
-40
lines changed

3 files changed

+177
-40
lines changed

src/Providers/Http/DTO/Request.php

Lines changed: 86 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
* method: string,
2121
* uri: string,
2222
* headers: array<string, list<string>>,
23-
* body?: string|null
23+
* data?: string|array<string, mixed>|null
2424
* }
2525
*
2626
* @extends AbstractDataTransferObject<RequestArrayShape>
@@ -30,7 +30,7 @@ class Request extends AbstractDataTransferObject
3030
public const KEY_METHOD = 'method';
3131
public const KEY_URI = 'uri';
3232
public const KEY_HEADERS = 'headers';
33-
public const KEY_BODY = 'body';
33+
public const KEY_DATA = 'data';
3434

3535
/**
3636
* @var HttpMethodEnum The HTTP method.
@@ -48,9 +48,9 @@ class Request extends AbstractDataTransferObject
4848
protected array $headers;
4949

5050
/**
51-
* @var string|null The request body.
51+
* @var string|array<string, mixed>|null The request data.
5252
*/
53-
protected ?string $body;
53+
protected $data;
5454

5555
/**
5656
* Constructor.
@@ -60,11 +60,11 @@ class Request extends AbstractDataTransferObject
6060
* @param HttpMethodEnum $method The HTTP method.
6161
* @param string $uri The request URI.
6262
* @param array<string, string|list<string>> $headers The request headers.
63-
* @param string|null $body The request body.
63+
* @param string|array<string, mixed>|null $data The request data.
6464
*
6565
* @throws InvalidArgumentException If the URI is empty.
6666
*/
67-
public function __construct(HttpMethodEnum $method, string $uri, array $headers = [], ?string $body = null)
67+
public function __construct(HttpMethodEnum $method, string $uri, array $headers = [], $data = null)
6868
{
6969
if (empty($uri)) {
7070
throw new InvalidArgumentException('URI cannot be empty.');
@@ -73,7 +73,7 @@ public function __construct(HttpMethodEnum $method, string $uri, array $headers
7373
$this->method = $method;
7474
$this->uri = $uri;
7575
$this->headers = $this->normalizeHeaders($headers);
76-
$this->body = $body;
76+
$this->data = $data;
7777
}
7878

7979
/**
@@ -91,12 +91,20 @@ public function getMethod(): HttpMethodEnum
9191
/**
9292
* Gets the request URI.
9393
*
94+
* For GET requests with array data, appends the data as query parameters.
95+
*
9496
* @since n.e.x.t
9597
*
9698
* @return string The URI.
9799
*/
98100
public function getUri(): string
99101
{
102+
// If GET request with array data, append as query parameters
103+
if ($this->method === HttpMethodEnum::GET() && is_array($this->data) && !empty($this->data)) {
104+
$separator = strpos($this->uri, '?') === false ? '?' : '&';
105+
return $this->uri . $separator . http_build_query($this->data);
106+
}
107+
100108
return $this->uri;
101109
}
102110

@@ -115,13 +123,65 @@ public function getHeaders(): array
115123
/**
116124
* Gets the request body.
117125
*
126+
* For GET requests, returns null.
127+
* For POST/PUT/PATCH requests:
128+
* - If data is a string, returns it as-is
129+
* - If data is an array and Content-Type is JSON, returns JSON-encoded data
130+
* - If data is an array and Content-Type is form, returns URL-encoded data
131+
*
118132
* @since n.e.x.t
119133
*
120134
* @return string|null The body.
135+
* @throws JsonException If the data cannot be encoded to JSON.
121136
*/
122137
public function getBody(): ?string
123138
{
124-
return $this->body;
139+
// GET requests don't have a body
140+
if ($this->method === HttpMethodEnum::GET()) {
141+
return null;
142+
}
143+
144+
// If data is null, return null
145+
if ($this->data === null) {
146+
return null;
147+
}
148+
149+
// If data is already a string, return it as-is
150+
if (is_string($this->data)) {
151+
return $this->data;
152+
}
153+
154+
// If data is an array, encode based on content type
155+
if (is_array($this->data)) {
156+
$contentType = $this->getContentType();
157+
158+
// JSON encoding
159+
if ($contentType !== null && stripos($contentType, 'application/json') !== false) {
160+
return json_encode($this->data, JSON_THROW_ON_ERROR);
161+
}
162+
163+
// Default to URL encoding for forms
164+
return http_build_query($this->data);
165+
}
166+
167+
return null;
168+
}
169+
170+
/**
171+
* Gets the Content-Type header value.
172+
*
173+
* @since n.e.x.t
174+
*
175+
* @return string|null The Content-Type header value or null if not set.
176+
*/
177+
private function getContentType(): ?string
178+
{
179+
foreach ($this->headers as $name => $values) {
180+
if (strcasecmp($name, 'Content-Type') === 0) {
181+
return $values[0] ?? null;
182+
}
183+
}
184+
return null;
125185
}
126186

127187
/**
@@ -138,20 +198,20 @@ public function withHeader(string $name, $value): self
138198
$headers = $this->headers;
139199
$headers[$name] = is_array($value) ? array_values($value) : [$value];
140200

141-
return new self($this->method, $this->uri, $headers, $this->body);
201+
return new self($this->method, $this->uri, $headers, $this->data);
142202
}
143203

144204
/**
145-
* Returns a new instance with the specified body.
205+
* Returns a new instance with the specified data.
146206
*
147207
* @since n.e.x.t
148208
*
149-
* @param string $body The request body.
150-
* @return self A new instance with the body.
209+
* @param string|array<string, mixed> $data The request data.
210+
* @return self A new instance with the data.
151211
*/
152-
public function withBody(string $body): self
212+
public function withData($data): self
153213
{
154-
return new self($this->method, $this->uri, $this->headers, $body);
214+
return new self($this->method, $this->uri, $this->headers, $data);
155215
}
156216

157217
/**
@@ -172,29 +232,15 @@ private function normalizeHeaders(array $headers): array
172232
}
173233

174234
/**
175-
* Gets the request data as an array.
176-
*
177-
* Attempts to decode the body as JSON. Returns null if the body
178-
* is empty or not valid JSON.
235+
* Gets the request data.
179236
*
180237
* @since n.e.x.t
181238
*
182-
* @return array<string, mixed>|null The decoded data or null.
239+
* @return string|array<string, mixed>|null The request data.
183240
*/
184-
public function getData(): ?array
241+
public function getData()
185242
{
186-
if ($this->body === null || $this->body === '') {
187-
return null;
188-
}
189-
190-
$data = json_decode($this->body, true);
191-
192-
if (json_last_error() !== JSON_ERROR_NONE) {
193-
return null;
194-
}
195-
196-
/** @var array<string, mixed>|null $data */
197-
return is_array($data) ? $data : null;
243+
return $this->data;
198244
}
199245

200246
/**
@@ -223,9 +269,9 @@ public static function getJsonSchema(): array
223269
],
224270
'description' => 'The request headers.',
225271
],
226-
self::KEY_BODY => [
227-
'type' => ['string', 'null'],
228-
'description' => 'The request body.',
272+
self::KEY_DATA => [
273+
'type' => ['string', 'array', 'null'],
274+
'description' => 'The request data.',
229275
],
230276
],
231277
'required' => [self::KEY_METHOD, self::KEY_URI, self::KEY_HEADERS],
@@ -241,17 +287,17 @@ public static function getJsonSchema(): array
241287
*/
242288
public function toArray(): array
243289
{
244-
$data = [
290+
$array = [
245291
self::KEY_METHOD => $this->method->value,
246292
self::KEY_URI => $this->uri,
247293
self::KEY_HEADERS => $this->headers,
248294
];
249295

250-
if ($this->body !== null) {
251-
$data[self::KEY_BODY] = $this->body;
296+
if ($this->data !== null) {
297+
$array[self::KEY_DATA] = $this->data;
252298
}
253299

254-
return $data;
300+
return $array;
255301
}
256302

257303
/**
@@ -267,7 +313,7 @@ public static function fromArray(array $array): self
267313
HttpMethodEnum::from($array[self::KEY_METHOD]),
268314
$array[self::KEY_URI],
269315
$array[self::KEY_HEADERS] ?? [],
270-
$array[self::KEY_BODY] ?? null
316+
$array[self::KEY_DATA] ?? null
271317
);
272318
}
273319
}

src/Providers/Http/DTO/Response.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,19 @@ public function getBody(): ?string
139139
return $this->body;
140140
}
141141

142+
/**
143+
* Checks if the response has a header.
144+
*
145+
* @since n.e.x.t
146+
*
147+
* @param string $name The header name.
148+
* @return bool True if the header exists, false otherwise.
149+
*/
150+
public function hasHeader(string $name): bool
151+
{
152+
return isset($this->headersMap[strtolower($name)]);
153+
}
154+
142155
/**
143156
* Checks if the response indicates success.
144157
*

tests/unit/Providers/Http/HttpTransporterTest.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,84 @@ public function testMultipleHeaderValues(): void
158158
$this->assertEquals(['single-value'], $sentRequest->getHeader('X-Custom'));
159159
}
160160

161+
/**
162+
* Tests sending a GET request with array data as query parameters.
163+
*
164+
* @covers ::send
165+
* @covers ::convertToPsr7Request
166+
*
167+
* @return void
168+
*/
169+
public function testSendGetRequestWithArrayData(): void
170+
{
171+
// Arrange
172+
$data = ['search' => 'test', 'limit' => '10'];
173+
$request = new Request(HttpMethodEnum::GET(), 'https://api.example.com/search', [], $data);
174+
175+
$mockResponse = new Psr7Response(200, [], '[]');
176+
$this->mockClient->addResponse($mockResponse);
177+
178+
// Act
179+
$response = $this->transporter->send($request);
180+
181+
// Assert
182+
$sentRequest = $this->mockClient->getRequests()[0];
183+
$this->assertEquals('https://api.example.com/search?search=test&limit=10', (string) $sentRequest->getUri());
184+
$this->assertEmpty((string) $sentRequest->getBody());
185+
}
186+
187+
/**
188+
* Tests sending a POST request with array data as JSON.
189+
*
190+
* @covers ::send
191+
* @covers ::convertToPsr7Request
192+
*
193+
* @return void
194+
*/
195+
public function testSendPostRequestWithArrayDataAsJson(): void
196+
{
197+
// Arrange
198+
$headers = ['Content-Type' => 'application/json'];
199+
$data = ['name' => 'test', 'value' => 123];
200+
$request = new Request(HttpMethodEnum::POST(), 'https://api.example.com/create', $headers, $data);
201+
202+
$mockResponse = new Psr7Response(201, [], '{"id":1}');
203+
$this->mockClient->addResponse($mockResponse);
204+
205+
// Act
206+
$response = $this->transporter->send($request);
207+
208+
// Assert
209+
$sentRequest = $this->mockClient->getRequests()[0];
210+
$this->assertEquals('{"name":"test","value":123}', (string) $sentRequest->getBody());
211+
}
212+
213+
/**
214+
* Tests sending a POST request with array data as form-encoded.
215+
*
216+
* @covers ::send
217+
* @covers ::convertToPsr7Request
218+
*
219+
* @return void
220+
*/
221+
public function testSendPostRequestWithArrayDataAsForm(): void
222+
{
223+
// Arrange
224+
$headers = ['Content-Type' => 'application/x-www-form-urlencoded'];
225+
$data = ['name' => 'test', 'value' => '123'];
226+
$request = new Request(HttpMethodEnum::POST(), 'https://api.example.com/create', $headers, $data);
227+
228+
$mockResponse = new Psr7Response(201, [], '{"id":1}');
229+
$this->mockClient->addResponse($mockResponse);
230+
231+
// Act
232+
$response = $this->transporter->send($request);
233+
234+
// Assert
235+
$sentRequest = $this->mockClient->getRequests()[0];
236+
$this->assertEquals('name=test&value=123', (string) $sentRequest->getBody());
237+
}
238+
161239
/**
162240
* Tests using discovery when no dependencies provided.
163241
*

0 commit comments

Comments
 (0)