diff --git a/.github/workflows/grumphp.yaml b/.github/workflows/grumphp.yaml index d6e5293..08afacf 100644 --- a/.github/workflows/grumphp.yaml +++ b/.github/workflows/grumphp.yaml @@ -29,7 +29,7 @@ jobs: id: composercache run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ steps.composercache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} diff --git a/docs/transports.md b/docs/transports.md index 50157fd..7dfc307 100644 --- a/docs/transports.md +++ b/docs/transports.md @@ -27,18 +27,20 @@ Examples: This package contains some frequently used encoders / decoders for you: -| Class | EncodingType | Action | -|---------------------|---------------------------------------|-------------------------------------------------------------------------------------| -| `EmptyBodyEncoder` | `EncoderInterface` | Creates epmty request body | -| `BinaryFileDecoder` | `DecoderInterface` | Parses file information from the HTTP response and returns a `BinaryFile` DTO | -| `JsonEncoder` | `EncoderInterface` | Adds json body and headers to request | -| `JsonDecoder` | `DecoderInterface` | Converts json response body to array | -| `MultiPartEncoder` | `EncoderInterface` | Adds symfony/mime `AbstractMultipartPart`as HTTP body. Handy for form data + files. | -| `StreamEncoder` | `EncoderInterface` | Adds PSR-7 Stream as request body | -| `StreamDecoder` | `DecoderInterface` | Returns the PSR-7 Stream as response result | -| `RawEncoder` | `EncoderInterface` | Adds raw string as request body | -| `RawDecoder` | `DecoderInterface` | Returns the raw PSR-7 body string as response result | -| `ResponseDecoder` | `DecoderInterface` | Returns the received PSR-7 response as result | +| Class | EncodingType | Action | +|-------------------------|---------------------------------------|-------------------------------------------------------------------------------------| +| `EmptyBodyEncoder` | `EncoderInterface` | Creates empty request body | +| `BinaryFileDecoder` | `DecoderInterface` | Parses file information from the HTTP response and returns a `BinaryFile` DTO | +| `FormUrlencodedEncoder` | `EncoderInterface` | Adds form urlencoded body and headers to request | +| `FormUrlencodedDecoder` | `DecoderInterface` | Converts form urlencoded response body to array | +| `JsonEncoder` | `EncoderInterface` | Adds json body and headers to request | +| `JsonDecoder` | `DecoderInterface` | Converts json response body to array | +| `MultiPartEncoder` | `EncoderInterface` | Adds symfony/mime `AbstractMultipartPart`as HTTP body. Handy for form data + files. | +| `StreamEncoder` | `EncoderInterface` | Adds PSR-7 Stream as request body | +| `StreamDecoder` | `DecoderInterface` | Returns the PSR-7 Stream as response result | +| `RawEncoder` | `EncoderInterface` | Adds raw string as request body | +| `RawDecoder` | `DecoderInterface` | Returns the raw PSR-7 body string as response result | +| `ResponseDecoder` | `DecoderInterface` | Returns the received PSR-7 response as result | ## Built-in transport presets: @@ -49,6 +51,7 @@ We've composed some of the encodings above into pre-configured transports: |------------------------|-------------------------|---------------------|------------------------| | `BinaryDownloadPreset` | `null` | `BinaryFile` | `withEmptyRequest` | | `BinaryDownloadPreset` | `AbstractMultipartPart` | `BinaryFile` | `withMultiPartRequest` | +| `FormUrlencodedPreset` | `?array` | `array` | `create` | | `JsonPreset` | `?array` | `array` | `create` | | `PsrPreset` | `string` | `ResponseInterface` | `create` | | `RawPreset` | `string` | `string` | `create` | diff --git a/src/Encoding/FormUrlencoded/FormUrlencodedDecoder.php b/src/Encoding/FormUrlencoded/FormUrlencodedDecoder.php new file mode 100644 index 0000000..476d200 --- /dev/null +++ b/src/Encoding/FormUrlencoded/FormUrlencodedDecoder.php @@ -0,0 +1,30 @@ + + */ +final class FormUrlencodedDecoder implements DecoderInterface +{ + public static function createWithAutodiscoveredPsrFactories(): self + { + return new self(); + } + + public function __invoke(ResponseInterface $response): array + { + if (!$responseBody = (string) $response->getBody()) { + return []; + } + + parse_str($responseBody, $output); + + return $output; + } +} diff --git a/src/Encoding/FormUrlencoded/FormUrlencodedEncoder.php b/src/Encoding/FormUrlencoded/FormUrlencodedEncoder.php new file mode 100644 index 0000000..bb6208b --- /dev/null +++ b/src/Encoding/FormUrlencoded/FormUrlencodedEncoder.php @@ -0,0 +1,45 @@ + + */ +final class FormUrlencodedEncoder implements EncoderInterface +{ + private StreamFactoryInterface $streamFactory; + + public function __construct(StreamFactoryInterface $streamFactory) + { + $this->streamFactory = $streamFactory; + } + + public static function createWithAutodiscoveredPsrFactories(): self + { + return new self( + Psr17FactoryDiscovery::findStreamFactory() + ); + } + + /** + * @param array|null $data + */ + public function __invoke(RequestInterface $request, $data): RequestInterface + { + return $request + ->withAddedHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody($this->streamFactory->createStream( + null !== $data ? http_build_query($data, encoding_type: PHP_QUERY_RFC1738) : '' + )); + } +} diff --git a/src/Transport/Presets/FormUrlencodedPreset.php b/src/Transport/Presets/FormUrlencodedPreset.php new file mode 100644 index 0000000..ea9f2a9 --- /dev/null +++ b/src/Transport/Presets/FormUrlencodedPreset.php @@ -0,0 +1,34 @@ +|null $decoder + * + * @return TransportInterface + */ + public static function create( + ClientInterface $client, + UriBuilderInterface $uriBuilder, + ?DecoderInterface $decoder = null, + ): TransportInterface { + return EncodedTransportFactory::create( + $client, + $uriBuilder, + FormUrlencodedEncoder::createWithAutodiscoveredPsrFactories(), + $decoder ?? FormUrlencodedDecoder::createWithAutodiscoveredPsrFactories() + ); + } +} diff --git a/tests/Unit/Encoding/FormUrlencoded/FormUrlencodedDecoderTest.php b/tests/Unit/Encoding/FormUrlencoded/FormUrlencodedDecoderTest.php new file mode 100644 index 0000000..3a10746 --- /dev/null +++ b/tests/Unit/Encoding/FormUrlencoded/FormUrlencodedDecoderTest.php @@ -0,0 +1,35 @@ +createResponse() + ->withBody($this->createStream('hello=world&foo=bar')); + $decoded = $decoder($response); + + self::assertSame(['hello' => 'world', 'foo' => 'bar'], $decoded); + } + + /** @test */ + public function it_can_decode_empty_body_to_empty_array(): void + { + $decoder = FormUrlencodedDecoder::createWithAutodiscoveredPsrFactories(); + $response = $this->createResponse()->withBody($this->createStream('')); + $decoded = $decoder($response); + + self::assertSame([], $decoded); + } +} diff --git a/tests/Unit/Encoding/FormUrlencoded/FormUrlencodedEncoderTest.php b/tests/Unit/Encoding/FormUrlencoded/FormUrlencodedEncoderTest.php new file mode 100644 index 0000000..d270aa1 --- /dev/null +++ b/tests/Unit/Encoding/FormUrlencoded/FormUrlencodedEncoderTest.php @@ -0,0 +1,44 @@ + 'world']; + $encoder = FormUrlencodedEncoder::createWithAutodiscoveredPsrFactories(); + $request = $this->createRequest('POST', '/hello'); + + $actual = $encoder($request, $data); + + self::assertSame($request->getMethod(), $actual->getMethod()); + self::assertSame($request->getUri(), $actual->getUri()); + self::assertSame('hello=world', (string) $actual->getBody()); + self::assertSame(['application/x-www-form-urlencoded'], $actual->getHeader('Content-Type')); + } + + /** @test */ + public function it_can_encode_null_to_empty_body(): void + { + $data = null; + $encoder = FormUrlencodedEncoder::createWithAutodiscoveredPsrFactories(); + $request = $this->createRequest('POST', '/hello'); + + $actual = $encoder($request, $data); + + self::assertSame($request->getMethod(), $actual->getMethod()); + self::assertSame($request->getUri(), $actual->getUri()); + self::assertSame('', (string) $actual->getBody()); + self::assertSame(['application/x-www-form-urlencoded'], $actual->getHeader('Content-Type')); + } +} diff --git a/tests/Unit/Transport/Presets/FormUrlencodedPresetTest.php b/tests/Unit/Transport/Presets/FormUrlencodedPresetTest.php new file mode 100644 index 0000000..e690caf --- /dev/null +++ b/tests/Unit/Transport/Presets/FormUrlencodedPresetTest.php @@ -0,0 +1,69 @@ +mockClient(), + RawUriBuilder::createWithAutodiscoveredPsrFactories() + ); + + $request = $this->createToolsRequest('GET', '/api', [], $expectedRequest = ['hello' => 'world']); + + $client->addResponse( + $this->createResponse(200) + ->withBody($this->createStream( + http_build_query($expectedResponse = ['foo' => 'bar'])) + ) + ); + + $actualResponse = $transport($request); + $lastRequest = $client->getLastRequest(); + + self::assertSame($actualResponse, $expectedResponse); + self::assertSame(http_build_query($expectedRequest), (string) $lastRequest->getBody()); + } + + #[Test] + public function it_is_possible_to_override_specific_decoder(): void + { + $transport = FormUrlencodedPreset::create( + $client = $this->mockClient(), + RawUriBuilder::createWithAutodiscoveredPsrFactories(), + JsonDecoder::createWithAutodiscoveredPsrFactories(), + ); + + $request = $this->createToolsRequest('GET', '/api', [], $expectedRequest = ['hello' => 'world']); + + $client->addResponse( + $this->createResponse(200) + ->withBody($this->createStream( + Json\encode($expectedResponse = ['foo' => 'bar'])) + ) + ); + + $actualResponse = $transport($request); + $lastRequest = $client->getLastRequest(); + + self::assertSame($actualResponse, $expectedResponse); + self::assertSame(http_build_query($expectedRequest), (string) $lastRequest->getBody()); + } +}