Skip to content

Commit 03a4630

Browse files
committed
Introduce form urlencoded transport
1 parent 7865f4c commit 03a4630

8 files changed

Lines changed: 274 additions & 13 deletions

File tree

.github/workflows/grumphp.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
id: composercache
3030
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
3131
- name: Cache dependencies
32-
uses: actions/cache@v2
32+
uses: actions/cache@v4
3333
with:
3434
path: ${{ steps.composercache.outputs.dir }}
3535
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}

docs/transports.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,21 @@ Examples:
2727

2828
This package contains some frequently used encoders / decoders for you:
2929

30-
| Class | EncodingType<DataType> | Action |
31-
|---------------------|---------------------------------------|-------------------------------------------------------------------------------------|
32-
| `EmptyBodyEncoder` | `EncoderInterface<null>` | Creates epmty request body |
33-
| `BinaryFileDecoder` | `DecoderInterface<BinaryFile>` | Parses file information from the HTTP response and returns a `BinaryFile` DTO |
34-
| `JsonEncoder` | `EncoderInterface<?array>` | Adds json body and headers to request |
35-
| `JsonDecoder` | `DecoderInterface<array>` | Converts json response body to array |
36-
| `MultiPartEncoder` | `EncoderInterface<AbstractMultipartPart>` | Adds symfony/mime `AbstractMultipartPart`as HTTP body. Handy for form data + files. |
37-
| `StreamEncoder` | `EncoderInterface<StreamInterface>` | Adds PSR-7 Stream as request body |
38-
| `StreamDecoder` | `DecoderInterface<StreamInterface>` | Returns the PSR-7 Stream as response result |
39-
| `RawEncoder` | `EncoderInterface<string>` | Adds raw string as request body |
40-
| `RawDecoder` | `DecoderInterface<string>` | Returns the raw PSR-7 body string as response result |
41-
| `ResponseDecoder` | `DecoderInterface<ResponseInterface>` | Returns the received PSR-7 response as result |
30+
| Class | EncodingType<DataType> | Action |
31+
|-------------------------|---------------------------------------|-------------------------------------------------------------------------------------|
32+
| `EmptyBodyEncoder` | `EncoderInterface<null>` | Creates epmty request body |
33+
| `BinaryFileDecoder` | `DecoderInterface<BinaryFile>` | Parses file information from the HTTP response and returns a `BinaryFile` DTO |
34+
| `FormUrlencodedDecoder` | `DecoderInterface<array>` | Converts form urlencoded response body to array |
35+
| `FormUrlencodedEncoder` | `EncoderInterface<?array>` | Adds form urlencoded body and headers to request |
36+
| `JsonDecoder` | `DecoderInterface<array>` | Converts json response body to array |
37+
| `JsonEncoder` | `EncoderInterface<?array>` | Adds json body and headers to request |
38+
| `JsonDecoder` | `DecoderInterface<array>` | Converts json response body to array |
39+
| `MultiPartEncoder` | `EncoderInterface<AbstractMultipartPart>` | Adds symfony/mime `AbstractMultipartPart`as HTTP body. Handy for form data + files. |
40+
| `StreamEncoder` | `EncoderInterface<StreamInterface>` | Adds PSR-7 Stream as request body |
41+
| `StreamDecoder` | `DecoderInterface<StreamInterface>` | Returns the PSR-7 Stream as response result |
42+
| `RawEncoder` | `EncoderInterface<string>` | Adds raw string as request body |
43+
| `RawDecoder` | `DecoderInterface<string>` | Returns the raw PSR-7 body string as response result |
44+
| `ResponseDecoder` | `DecoderInterface<ResponseInterface>` | Returns the received PSR-7 response as result |
4245

4346
## Built-in transport presets:
4447

@@ -49,6 +52,7 @@ We've composed some of the encodings above into pre-configured transports:
4952
|------------------------|-------------------------|---------------------|------------------------|
5053
| `BinaryDownloadPreset` | `null` | `BinaryFile` | `withEmptyRequest` |
5154
| `BinaryDownloadPreset` | `AbstractMultipartPart` | `BinaryFile` | `withMultiPartRequest` |
55+
| `FormUrlencodedPreset` | `?array` | `array` | `create` |
5256
| `JsonPreset` | `?array` | `array` | `create` |
5357
| `PsrPreset` | `string` | `ResponseInterface` | `create` |
5458
| `RawPreset` | `string` | `string` | `create` |
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phpro\HttpTools\Encoding\FormUrlencoded;
6+
7+
use Phpro\HttpTools\Encoding\DecoderInterface;
8+
use Psr\Http\Message\ResponseInterface;
9+
10+
/**
11+
* @implements DecoderInterface<array>
12+
*/
13+
final class FormUrlencodedDecoder implements DecoderInterface
14+
{
15+
public static function createWithAutodiscoveredPsrFactories(): self
16+
{
17+
return new self();
18+
}
19+
20+
public function __invoke(ResponseInterface $response): array
21+
{
22+
if (!$responseBody = (string) $response->getBody()) {
23+
return [];
24+
}
25+
26+
parse_str($responseBody, $output);
27+
28+
return $output;
29+
}
30+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phpro\HttpTools\Encoding\FormUrlencoded;
6+
7+
use Http\Discovery\Psr17FactoryDiscovery;
8+
9+
use const PHP_QUERY_RFC1738;
10+
11+
use Phpro\HttpTools\Encoding\EncoderInterface;
12+
use Psr\Http\Message\RequestInterface;
13+
use Psr\Http\Message\StreamFactoryInterface;
14+
15+
/**
16+
* @implements EncoderInterface<array|null>
17+
*/
18+
final class FormUrlencodedEncoder implements EncoderInterface
19+
{
20+
private StreamFactoryInterface $streamFactory;
21+
22+
public function __construct(StreamFactoryInterface $streamFactory)
23+
{
24+
$this->streamFactory = $streamFactory;
25+
}
26+
27+
public static function createWithAutodiscoveredPsrFactories(): self
28+
{
29+
return new self(
30+
Psr17FactoryDiscovery::findStreamFactory()
31+
);
32+
}
33+
34+
/**
35+
* @param array|null $data
36+
*/
37+
public function __invoke(RequestInterface $request, $data): RequestInterface
38+
{
39+
return $request
40+
->withAddedHeader('Content-Type', 'application/x-www-form-urlencoded')
41+
->withBody($this->streamFactory->createStream(
42+
null !== $data ? http_build_query($data, encoding_type: PHP_QUERY_RFC1738) : ''
43+
));
44+
}
45+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phpro\HttpTools\Transport\Presets;
6+
7+
use Phpro\HttpTools\Encoding\DecoderInterface;
8+
use Phpro\HttpTools\Encoding\FormUrlencoded\FormUrlencodedDecoder;
9+
use Phpro\HttpTools\Encoding\FormUrlencoded\FormUrlencodedEncoder;
10+
use Phpro\HttpTools\Transport\EncodedTransportFactory;
11+
use Phpro\HttpTools\Transport\TransportInterface;
12+
use Phpro\HttpTools\Uri\UriBuilderInterface;
13+
use Psr\Http\Client\ClientInterface;
14+
15+
final class FormUrlencodedPreset
16+
{
17+
/**
18+
* @param DecoderInterface<array>|null $decoder
19+
*
20+
* @return TransportInterface<array|null, array>
21+
*/
22+
public static function create(
23+
ClientInterface $client,
24+
UriBuilderInterface $uriBuilder,
25+
?DecoderInterface $decoder = null,
26+
): TransportInterface {
27+
return EncodedTransportFactory::create(
28+
$client,
29+
$uriBuilder,
30+
FormUrlencodedEncoder::createWithAutodiscoveredPsrFactories(),
31+
$decoder ?? FormUrlencodedDecoder::createWithAutodiscoveredPsrFactories()
32+
);
33+
}
34+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phpro\HttpTools\Tests\Unit\Encoding\FormUrlencoded;
6+
7+
use Phpro\HttpTools\Encoding\FormUrlencoded\FormUrlencodedDecoder;
8+
use Phpro\HttpTools\Test\UseHttpFactories;
9+
use PHPUnit\Framework\TestCase;
10+
11+
final class FormUrlencodedDecoderTest extends TestCase
12+
{
13+
use UseHttpFactories;
14+
15+
/** @test */
16+
public function it_can_decode_form_url_encoded_to_array(): void
17+
{
18+
$decoder = FormUrlencodedDecoder::createWithAutodiscoveredPsrFactories();
19+
$response = $this->createResponse()
20+
->withBody($this->createStream('hello=world&foo=bar'));
21+
$decoded = $decoder($response);
22+
23+
self::assertSame(['hello' => 'world', 'foo' => 'bar'], $decoded);
24+
}
25+
26+
/** @test */
27+
public function it_can_decode_empty_body_to_empty_array(): void
28+
{
29+
$decoder = FormUrlencodedDecoder::createWithAutodiscoveredPsrFactories();
30+
$response = $this->createResponse()->withBody($this->createStream(''));
31+
$decoded = $decoder($response);
32+
33+
self::assertSame([], $decoded);
34+
}
35+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phpro\HttpTools\Tests\Unit\Encoding\FormUrlencoded;
6+
7+
use Phpro\HttpTools\Encoding\FormUrlencoded\FormUrlencodedEncoder;
8+
use Phpro\HttpTools\Test\UseHttpFactories;
9+
use PHPUnit\Framework\TestCase;
10+
11+
final class FormUrlencodedEncoderTest extends TestCase
12+
{
13+
use UseHttpFactories;
14+
15+
/** @test */
16+
public function it_can_encode_array_to_url_encoded(): void
17+
{
18+
$data = ['hello' => 'world'];
19+
$encoder = FormUrlencodedEncoder::createWithAutodiscoveredPsrFactories();
20+
$request = $this->createRequest('POST', '/hello');
21+
22+
$actual = $encoder($request, $data);
23+
24+
self::assertSame($request->getMethod(), $actual->getMethod());
25+
self::assertSame($request->getUri(), $actual->getUri());
26+
self::assertSame('hello=world', (string) $actual->getBody());
27+
self::assertSame(['application/x-www-form-urlencoded'], $actual->getHeader('Content-Type'));
28+
}
29+
30+
/** @test */
31+
public function it_can_encode_null_to_empty_body(): void
32+
{
33+
$data = null;
34+
$encoder = FormUrlencodedEncoder::createWithAutodiscoveredPsrFactories();
35+
$request = $this->createRequest('POST', '/hello');
36+
37+
$actual = $encoder($request, $data);
38+
39+
self::assertSame($request->getMethod(), $actual->getMethod());
40+
self::assertSame($request->getUri(), $actual->getUri());
41+
self::assertSame('', (string) $actual->getBody());
42+
self::assertSame(['application/x-www-form-urlencoded'], $actual->getHeader('Content-Type'));
43+
}
44+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phpro\HttpTools\Tests\Unit\Transport\Presets;
6+
7+
use Phpro\HttpTools\Encoding\Json\JsonDecoder;
8+
use Phpro\HttpTools\Test\UseHttpToolsFactories;
9+
use Phpro\HttpTools\Test\UseMockClient;
10+
use Phpro\HttpTools\Transport\Presets\FormUrlencodedPreset;
11+
use Phpro\HttpTools\Uri\RawUriBuilder;
12+
use PHPUnit\Framework\Attributes\Test;
13+
use PHPUnit\Framework\TestCase;
14+
use Psl\Json;
15+
16+
final class FormUrlencodedPresetTest extends TestCase
17+
{
18+
use UseHttpToolsFactories;
19+
use UseMockClient;
20+
21+
/** @test */
22+
public function it_can_create_transport(): void
23+
{
24+
$transport = FormUrlencodedPreset::create(
25+
$client = $this->mockClient(),
26+
RawUriBuilder::createWithAutodiscoveredPsrFactories()
27+
);
28+
29+
$request = $this->createToolsRequest('GET', '/api', [], $expectedRequest = ['hello' => 'world']);
30+
31+
$client->addResponse(
32+
$this->createResponse(200)
33+
->withBody($this->createStream(
34+
http_build_query($expectedResponse = ['foo' => 'bar']))
35+
)
36+
);
37+
38+
$actualResponse = $transport($request);
39+
$lastRequest = $client->getLastRequest();
40+
41+
self::assertSame($actualResponse, $expectedResponse);
42+
self::assertSame(http_build_query($expectedRequest), (string) $lastRequest->getBody());
43+
}
44+
45+
#[Test]
46+
public function it_is_possible_to_override_specific_decoder(): void
47+
{
48+
$transport = FormUrlencodedPreset::create(
49+
$client = $this->mockClient(),
50+
RawUriBuilder::createWithAutodiscoveredPsrFactories(),
51+
JsonDecoder::createWithAutodiscoveredPsrFactories(),
52+
);
53+
54+
$request = $this->createToolsRequest('GET', '/api', [], $expectedRequest = ['hello' => 'world']);
55+
56+
$client->addResponse(
57+
$this->createResponse(200)
58+
->withBody($this->createStream(
59+
Json\encode($expectedResponse = ['foo' => 'bar']))
60+
)
61+
);
62+
63+
$actualResponse = $transport($request);
64+
$lastRequest = $client->getLastRequest();
65+
66+
self::assertSame($actualResponse, $expectedResponse);
67+
self::assertSame(http_build_query($expectedRequest), (string) $lastRequest->getBody());
68+
}
69+
}

0 commit comments

Comments
 (0)