├── docs
├── assets
│ ├── request-handlers.png
│ └── request-handlers.dot
├── sdk.md
├── files.md
└── transports.md
├── src
├── Client
│ ├── Factory
│ │ ├── FactoryInterface.php
│ │ ├── AutoDiscoveredClientFactory.php
│ │ ├── SymfonyClientFactory.php
│ │ ├── GuzzleClientFactory.php
│ │ └── LazyClientLoader.php
│ ├── Configurator
│ │ └── PluginsConfigurator.php
│ └── ClientBuilder.php
├── Uri
│ ├── UriBuilderInterface.php
│ ├── TemplatedUriBuilder.php
│ └── RawUriBuilder.php
├── Encoding
│ ├── DecoderInterface.php
│ ├── EncoderInterface.php
│ ├── Raw
│ │ ├── RawDecoder.php
│ │ ├── RawEncoder.php
│ │ └── EmptyBodyEncoder.php
│ ├── Psr7
│ │ └── ResponseDecoder.php
│ ├── Stream
│ │ ├── StreamDecoder.php
│ │ └── StreamEncoder.php
│ ├── Json
│ │ ├── JsonDecoder.php
│ │ └── JsonEncoder.php
│ ├── FormUrlencoded
│ │ ├── FormUrlencodedDecoder.php
│ │ └── FormUrlencodedEncoder.php
│ ├── Binary
│ │ ├── Extractor
│ │ │ ├── FilenameExtractor.php
│ │ │ ├── SizeExtractor.php
│ │ │ ├── HashExtractor.php
│ │ │ ├── ExtensionExtractor.php
│ │ │ └── MimeTypeExtractor.php
│ │ ├── BinaryFile.php
│ │ └── BinaryFileDecoder.php
│ └── Mime
│ │ └── MultiPartEncoder.php
├── Exception
│ └── RuntimeException.php
├── Serializer
│ ├── SerializerInterface.php
│ ├── SerializerException.php
│ └── SymfonySerializer.php
├── Dependency
│ ├── MockClientDependency.php
│ ├── GuzzleDependency.php
│ ├── VcrPluginDependency.php
│ ├── SymfonyMimeDependency.php
│ └── SymfonyClientDependency.php
├── Transport
│ ├── TransportInterface.php
│ ├── IO
│ │ └── Input
│ │ │ ├── RequestConverterInterface.php
│ │ │ └── EncodingRequestConverter.php
│ ├── Presets
│ │ ├── RawPreset.php
│ │ ├── JsonPreset.php
│ │ ├── PsrPreset.php
│ │ ├── FormUrlencodedPreset.php
│ │ └── BinaryDownloadPreset.php
│ ├── EncodedTransportFactory.php
│ ├── CallbackTransport.php
│ └── Serializer
│ │ └── SerializerTransport.php
├── Formatter
│ ├── Factory
│ │ └── BasicFormatterFactory.php
│ ├── FormatterBuilder.php
│ ├── RemoveSensitiveQueryStringsFormatter.php
│ ├── RemoveSensitiveHeadersFormatter.php
│ └── RemoveSensitiveJsonKeysFormatter.php
├── Request
│ ├── RequestInterface.php
│ └── Request.php
├── Test
│ ├── UseMockClient.php
│ ├── UseHttpToolsFactories.php
│ ├── UseVcrClient.php
│ └── UseHttpFactories.php
├── Sdk
│ ├── Rest
│ │ ├── CreateTrait.php
│ │ ├── FindTrait.php
│ │ ├── GetTrait.php
│ │ ├── DeleteTrait.php
│ │ ├── PatchTrait.php
│ │ └── UpdateTrait.php
│ └── HttpResource.php
└── Plugin
│ ├── AcceptLanguagePlugin.php
│ └── CallbackPlugin.php
├── grumphp.yml
├── psalm.xml
├── LICENSE
├── static-analysis
└── Transport
│ └── Serializer
│ └── serializer-transport.php
├── examples
└── sdk.php
├── composer.json
├── .php-cs-fixer.dist.php
└── README.md
/docs/assets/request-handlers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phpro/http-tools/HEAD/docs/assets/request-handlers.png
--------------------------------------------------------------------------------
/src/Client/Factory/FactoryInterface.php:
--------------------------------------------------------------------------------
1 | $class
15 | *
16 | * @return C
17 | */
18 | public function deserialize(string $data, string $class);
19 | }
20 |
--------------------------------------------------------------------------------
/src/Serializer/SerializerException.php:
--------------------------------------------------------------------------------
1 | $request
17 | *
18 | * @return ResponseType
19 | */
20 | public function __invoke(RequestInterface $request);
21 | }
22 |
--------------------------------------------------------------------------------
/src/Dependency/GuzzleDependency.php:
--------------------------------------------------------------------------------
1 | 7.'
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Dependency/VcrPluginDependency.php:
--------------------------------------------------------------------------------
1 | $request
17 | */
18 | public function __invoke(RequestInterface $request): PsrRequestInterface;
19 | }
20 |
--------------------------------------------------------------------------------
/src/Dependency/SymfonyMimeDependency.php:
--------------------------------------------------------------------------------
1 | 6.0'
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Formatter/Factory/BasicFormatterFactory.php:
--------------------------------------------------------------------------------
1 | 5.4'
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/grumphp.yml:
--------------------------------------------------------------------------------
1 | grumphp:
2 | tasks:
3 | phpcsfixer2:
4 | config: ".php-cs-fixer.dist.php"
5 | config_contains_finder: true
6 | psalm:
7 | config: psalm.xml
8 | show_info: true
9 | no_cache: true
10 | phpunit: ~
11 | composer:
12 | metadata:
13 | blocking: false
14 | clover_coverage:
15 | clover_file: coverage/clover.xml
16 | level: 100
17 | metadata:
18 | priority: -100
19 | environment:
20 | paths:
21 | - 'tools'
22 |
--------------------------------------------------------------------------------
/src/Request/RequestInterface.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | final class RawDecoder implements DecoderInterface
14 | {
15 | public static function createWithAutodiscoveredPsrFactories(): self
16 | {
17 | return new self();
18 | }
19 |
20 | public function __invoke(ResponseInterface $response): string
21 | {
22 | return (string) $response->getBody();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Test/UseMockClient.php:
--------------------------------------------------------------------------------
1 | $client;
21 |
22 | return $configurator(new Client());
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Encoding/Psr7/ResponseDecoder.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | final class ResponseDecoder implements DecoderInterface
14 | {
15 | public static function createWithAutodiscoveredPsrFactories(): self
16 | {
17 | return new self();
18 | }
19 |
20 | public function __invoke(ResponseInterface $response): ResponseInterface
21 | {
22 | return $response;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Encoding/Stream/StreamDecoder.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | final class StreamDecoder implements DecoderInterface
15 | {
16 | public static function createWithAutodiscoveredPsrFactories(): self
17 | {
18 | return new self();
19 | }
20 |
21 | public function __invoke(ResponseInterface $response): StreamInterface
22 | {
23 | return $response->getBody();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Test/UseHttpToolsFactories.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | private function createToolsRequest(
21 | string $method,
22 | string $uri,
23 | array $uriParams = [],
24 | $body = null,
25 | ): RequestInterface {
26 | return new Request($method, $uri, $uriParams, $body);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/docs/assets/request-handlers.dot:
--------------------------------------------------------------------------------
1 | digraph RequestHandlers {
2 |
3 | subgraph cluster_App {
4 | label="HTTP Value objects";
5 |
6 | Response [label="Response model"];
7 | Request [label="Request model"];
8 | }
9 |
10 | Encoder [label="Encode HTTP data"]
11 | Decoder [label="Decode HTTP data"]
12 | Transport
13 | RequestHandler;
14 | PSRHttpClient [label="PSR-18 HTTP Client"]
15 |
16 | {rank = same; Decoder Encoder }
17 |
18 | Transport -> RequestHandler
19 | RequestHandler -> Transport
20 | Transport -> Encoder
21 | Encoder -> PSRHttpClient
22 | Decoder -> Transport
23 | PSRHttpClient -> Decoder
24 | RequestHandler -> Response
25 | Request -> RequestHandler
26 | }
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/Encoding/Stream/StreamEncoder.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | final class StreamEncoder implements EncoderInterface
15 | {
16 | public static function createWithAutodiscoveredPsrFactories(): self
17 | {
18 | return new self();
19 | }
20 |
21 | /**
22 | * @param StreamInterface $data
23 | */
24 | public function __invoke(RequestInterface $request, $data): RequestInterface
25 | {
26 | return $request->withBody($data);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Encoding/Json/JsonDecoder.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | final class JsonDecoder implements DecoderInterface
15 | {
16 | public static function createWithAutodiscoveredPsrFactories(): self
17 | {
18 | return new self();
19 | }
20 |
21 | public function __invoke(ResponseInterface $response): array
22 | {
23 | if (!$responseBody = (string) $response->getBody()) {
24 | return [];
25 | }
26 |
27 | return (array) Json\decode($responseBody, true);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Sdk/Rest/CreateTrait.php:
--------------------------------------------------------------------------------
1 | $request */
26 | $request = new Request('POST', $this->path(), [], $data);
27 |
28 | return $this->transport()($request);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Sdk/Rest/FindTrait.php:
--------------------------------------------------------------------------------
1 | $request */
26 | $request = new Request('GET', $this->path().'/'.$identifier, [], null);
27 |
28 | return $this->transport()($request);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Sdk/Rest/GetTrait.php:
--------------------------------------------------------------------------------
1 | $request */
26 | $request = new Request('GET', $this->path(), $uriParameters, null);
27 |
28 | return $this->transport()($request);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Uri/TemplatedUriBuilder.php:
--------------------------------------------------------------------------------
1 | defaultVariables = $defaultVariables;
19 | }
20 |
21 | public function __invoke(RequestInterface $request): UriInterface
22 | {
23 | $uriTemplate = new UriTemplate($request->uri(), $this->defaultVariables);
24 |
25 | return Http::new($uriTemplate->expand($request->uriParameters()));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Encoding/FormUrlencoded/FormUrlencodedDecoder.php:
--------------------------------------------------------------------------------
1 |
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 | }
31 |
--------------------------------------------------------------------------------
/src/Plugin/AcceptLanguagePlugin.php:
--------------------------------------------------------------------------------
1 | acceptLanguage = $acceptLanguage;
18 | }
19 |
20 | public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
21 | {
22 | return $next(
23 | $request->withAddedHeader(
24 | 'Accept-Language',
25 | $this->acceptLanguage
26 | )
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Sdk/Rest/DeleteTrait.php:
--------------------------------------------------------------------------------
1 | $request */
26 | $request = new Request('DELETE', $this->path().'/'.$identifier, [], null);
27 |
28 | return $this->transport()($request);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Client/Factory/AutoDiscoveredClientFactory.php:
--------------------------------------------------------------------------------
1 | $options
18 | * @param list $middlewares
19 | */
20 | public static function create(iterable $middlewares, array $options = []): ClientInterface
21 | {
22 | return PluginsConfigurator::configure(Psr18ClientDiscovery::find(), $middlewares);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Encoding/Binary/Extractor/FilenameExtractor.php:
--------------------------------------------------------------------------------
1 | getHeader('Content-Disposition'));
19 | if (null === $disposition) {
20 | return null;
21 | }
22 |
23 | return try_catch(
24 | static fn () => ContentDisposition::parse($disposition)->getFilename(),
25 | static fn () => null
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Sdk/Rest/PatchTrait.php:
--------------------------------------------------------------------------------
1 | $request */
26 | $request = new Request('PATCH', $this->path().'/'.$identifier, [], $data);
27 |
28 | return $this->transport()($request);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Sdk/Rest/UpdateTrait.php:
--------------------------------------------------------------------------------
1 | $request */
26 | $request = new Request('PUT', $this->path().'/'.$identifier, [], $data);
27 |
28 | return $this->transport()($request);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Encoding/Binary/Extractor/SizeExtractor.php:
--------------------------------------------------------------------------------
1 | getBody()->getSize();
18 | if (null !== $size) {
19 | return $size;
20 | }
21 |
22 | $length = first($response->getHeader('Content-Length'));
23 | if (null !== $length) {
24 | return try_catch(
25 | static fn () => int()->coerce($length),
26 | static fn () => null
27 | );
28 | }
29 |
30 | return null;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Sdk/HttpResource.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | private TransportInterface $transport;
18 |
19 | /**
20 | * @param TransportInterface $transport
21 | */
22 | public function __construct(TransportInterface $transport)
23 | {
24 | $this->transport = $transport;
25 | }
26 |
27 | abstract protected function path(): string;
28 |
29 | /**
30 | * @return TransportInterface
31 | */
32 | protected function transport(): TransportInterface
33 | {
34 | return $this->transport;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Client/Configurator/PluginsConfigurator.php:
--------------------------------------------------------------------------------
1 | $plugins
20 | *
21 | * @return PluginClient
22 | */
23 | public static function configure(ClientInterface $client, iterable $plugins): ClientInterface
24 | {
25 | Assert::allIsInstanceOf($plugins, Plugin::class);
26 |
27 | return new PluginClient(
28 | $client,
29 | is_array($plugins) ? $plugins : iterator_to_array($plugins, false),
30 | []
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Encoding/Binary/Extractor/HashExtractor.php:
--------------------------------------------------------------------------------
1 | getBody();
21 |
22 | $pos = $body->tell();
23 | if ($pos > 0) {
24 | $body->rewind();
25 | }
26 |
27 | $context = Context::forAlgorithm($this->algorithm);
28 | while (!$body->eof()) {
29 | $context = $context->update($body->read(1048576));
30 | }
31 |
32 | $body->seek($pos);
33 |
34 | return $context->finalize();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Transport/Presets/RawPreset.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | public static function create(
20 | ClientInterface $client,
21 | UriBuilderInterface $uriBuilder,
22 | ): TransportInterface {
23 | return EncodedTransportFactory::create(
24 | $client,
25 | $uriBuilder,
26 | RawEncoder::createWithAutodiscoveredPsrFactories(),
27 | RawDecoder::createWithAutodiscoveredPsrFactories()
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Transport/Presets/JsonPreset.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | public static function create(
20 | ClientInterface $client,
21 | UriBuilderInterface $uriBuilder,
22 | ): TransportInterface {
23 | return EncodedTransportFactory::create(
24 | $client,
25 | $uriBuilder,
26 | JsonEncoder::createWithAutodiscoveredPsrFactories(),
27 | JsonDecoder::createWithAutodiscoveredPsrFactories()
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Plugin/CallbackPlugin.php:
--------------------------------------------------------------------------------
1 | callback = $callback;
28 | }
29 |
30 | /**
31 | * @param Step $next
32 | * @param Step $first
33 | */
34 | public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise
35 | {
36 | return ($this->callback)($request, $next, $first);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Client/Factory/SymfonyClientFactory.php:
--------------------------------------------------------------------------------
1 | $middlewares
20 | */
21 | public static function create(iterable $middlewares, array $options = []): ClientInterface
22 | {
23 | SymfonyClientDependency::guard();
24 |
25 | return PluginsConfigurator::configure(
26 | new HttplugClient(
27 | new CurlHttpClient($options)
28 | ),
29 | $middlewares
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Transport/Presets/PsrPreset.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | public static function create(
21 | ClientInterface $client,
22 | UriBuilderInterface $uriBuilder,
23 | ): TransportInterface {
24 | return EncodedTransportFactory::create(
25 | $client,
26 | $uriBuilder,
27 | RawEncoder::createWithAutodiscoveredPsrFactories(),
28 | ResponseDecoder::createWithAutodiscoveredPsrFactories()
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Client/Factory/GuzzleClientFactory.php:
--------------------------------------------------------------------------------
1 | $middlewares
19 | */
20 | public static function create(iterable $middlewares, array $options = []): ClientInterface
21 | {
22 | Assert::allIsCallable($middlewares);
23 | GuzzleDependency::guard();
24 |
25 | $stack = HandlerStack::create();
26 |
27 | foreach ($middlewares as $middleware) {
28 | $stack->push($middleware);
29 | }
30 |
31 | return new Client(
32 | array_merge($options, ['handler' => $stack])
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Encoding/Binary/Extractor/ExtensionExtractor.php:
--------------------------------------------------------------------------------
1 | getExtensions($mimeType);
22 | if ($extensions) {
23 | return first($extensions);
24 | }
25 | }
26 |
27 | $originalName = (new FilenameExtractor())($response);
28 | if (null !== $originalName) {
29 | return pathinfo($originalName, PATHINFO_EXTENSION) ?: null;
30 | }
31 |
32 | return null;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Uri/RawUriBuilder.php:
--------------------------------------------------------------------------------
1 | uriFactory = $uriFactory;
19 | }
20 |
21 | public static function createWithAutodiscoveredPsrFactories(): self
22 | {
23 | return new self(Psr17FactoryDiscovery::findUriFactory());
24 | }
25 |
26 | public function __invoke(RequestInterface $request): UriInterface
27 | {
28 | $uri = $this->uriFactory->createUri($request->uri());
29 |
30 | if (!$request->uriParameters()) {
31 | return $uri;
32 | }
33 |
34 | return $uri->withQuery(http_build_query($request->uriParameters()));
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Encoding/Raw/RawEncoder.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | final class RawEncoder implements EncoderInterface
16 | {
17 | private StreamFactoryInterface $streamFactory;
18 |
19 | public function __construct(StreamFactoryInterface $streamFactory)
20 | {
21 | $this->streamFactory = $streamFactory;
22 | }
23 |
24 | public static function createWithAutodiscoveredPsrFactories(): self
25 | {
26 | return new self(
27 | Psr17FactoryDiscovery::findStreamFactory()
28 | );
29 | }
30 |
31 | /**
32 | * @param string $data
33 | */
34 | public function __invoke(RequestInterface $request, $data): RequestInterface
35 | {
36 | return $request->withBody($this->streamFactory->createStream($data));
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Encoding/Raw/EmptyBodyEncoder.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | final class EmptyBodyEncoder implements EncoderInterface
16 | {
17 | private StreamFactoryInterface $streamFactory;
18 |
19 | public function __construct(StreamFactoryInterface $streamFactory)
20 | {
21 | $this->streamFactory = $streamFactory;
22 | }
23 |
24 | public static function createWithAutodiscoveredPsrFactories(): self
25 | {
26 | return new self(
27 | Psr17FactoryDiscovery::findStreamFactory()
28 | );
29 | }
30 |
31 | /**
32 | * @param null $data
33 | */
34 | public function __invoke(RequestInterface $request, $data): RequestInterface
35 | {
36 | return $request->withBody($this->streamFactory->createStream());
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Encoding/Binary/Extractor/MimeTypeExtractor.php:
--------------------------------------------------------------------------------
1 | getHeader('Content-Type'));
19 | if (null !== $contentType && '' !== $contentType) {
20 | return $contentType;
21 | }
22 |
23 | $originalName = (new FilenameExtractor())($response);
24 | if (null !== $originalName) {
25 | if ($extension = pathinfo($originalName, PATHINFO_EXTENSION)) {
26 | SymfonyMimeDependency::guard();
27 | $mimeTypes = MimeTypes::getDefault()->getMimeTypes($extension);
28 |
29 | return first($mimeTypes);
30 | }
31 | }
32 |
33 | return null;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Test/UseVcrClient.php:
--------------------------------------------------------------------------------
1 | infinite Phpro
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/Encoding/Binary/BinaryFile.php:
--------------------------------------------------------------------------------
1 | stream;
24 | }
25 |
26 | public function fileSizeInBytes(): ?int
27 | {
28 | return $this->fileSizeInBytes;
29 | }
30 |
31 | public function mimeType(): ?string
32 | {
33 | return $this->mimeType;
34 | }
35 |
36 | public function fileName(): ?string
37 | {
38 | return $this->fileName;
39 | }
40 |
41 | public function extension(): ?string
42 | {
43 | return $this->extension;
44 | }
45 |
46 | public function hash(): ?string
47 | {
48 | return $this->hash;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Transport/Presets/FormUrlencodedPreset.php:
--------------------------------------------------------------------------------
1 | |null $decoder
19 | *
20 | * @return TransportInterface
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 | }
35 |
--------------------------------------------------------------------------------
/src/Serializer/SymfonySerializer.php:
--------------------------------------------------------------------------------
1 | serializer = $serializer;
17 | $this->format = $format;
18 | }
19 |
20 | public function serialize(mixed $data): string
21 | {
22 | return $this->serializer->serialize($data, $this->format);
23 | }
24 |
25 | /**
26 | * @psalm-suppress UnnecessaryVarAnnotation - It knows $result ... Sometimes it does, sometimes it dont... :')
27 | *
28 | * @template C
29 | *
30 | * @param class-string $class
31 | *
32 | * @return C
33 | */
34 | public function deserialize(string $data, string $class)
35 | {
36 | /** @var C $result */
37 | $result = $this->serializer->deserialize($data, $class, $this->format);
38 |
39 | return $result;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/static-analysis/Transport/Serializer/serializer-transport.php:
--------------------------------------------------------------------------------
1 | $x
18 | *
19 | * @return null
20 | */
21 | function testEmptySerializer(SerializerTransport $x)
22 | {
23 | return $x(new Request('GET', '/', [], new Foo()));
24 | }
25 |
26 | /**
27 | * @param SerializerTransport $x
28 | */
29 | function testTargetSerializer(SerializerTransport $x): Foo
30 | {
31 | return $x(new Request('GET', '/', [], new Foo()));
32 | }
33 |
34 | /**
35 | * @param TransportInterface $transport
36 | */
37 | function test(SerializerInterface $serializer, TransportInterface $transport): void
38 | {
39 | /** @var SerializerTransport $serializerTransport */
40 | $serializerTransport = new SerializerTransport($serializer, $transport);
41 |
42 | testEmptySerializer($serializerTransport);
43 |
44 | testTargetSerializer($serializerTransport->withOutputType(Foo::class));
45 | }
46 |
--------------------------------------------------------------------------------
/src/Test/UseHttpFactories.php:
--------------------------------------------------------------------------------
1 | createRequest($method, $uri);
20 | }
21 |
22 | private static function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
23 | {
24 | return Psr17FactoryDiscovery::findResponseFactory()->createResponse($code, $reasonPhrase);
25 | }
26 |
27 | private static function createStream(string $content): StreamInterface
28 | {
29 | return Psr17FactoryDiscovery::findStreamFactory()->createStream($content);
30 | }
31 |
32 | private static function createEmptyHttpClientException(string $message): ClientExceptionInterface&Exception
33 | {
34 | return new class($message) extends RuntimeException implements ClientExceptionInterface, Exception {
35 | };
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Encoding/Json/JsonEncoder.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | final class JsonEncoder implements EncoderInterface
17 | {
18 | private StreamFactoryInterface $streamFactory;
19 |
20 | public function __construct(StreamFactoryInterface $streamFactory)
21 | {
22 | $this->streamFactory = $streamFactory;
23 | }
24 |
25 | public static function createWithAutodiscoveredPsrFactories(): self
26 | {
27 | return new self(
28 | Psr17FactoryDiscovery::findStreamFactory()
29 | );
30 | }
31 |
32 | /**
33 | * @param array|null $data
34 | */
35 | public function __invoke(RequestInterface $request, $data): RequestInterface
36 | {
37 | return $request
38 | ->withAddedHeader('Content-Type', 'application/json')
39 | ->withAddedHeader('Accept', 'application/json')
40 | ->withBody($this->streamFactory->createStream(
41 | null !== $data ? Json\encode($data) : ''
42 | ));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Encoding/FormUrlencoded/FormUrlencodedEncoder.php:
--------------------------------------------------------------------------------
1 |
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 | }
46 |
--------------------------------------------------------------------------------
/src/Client/Factory/LazyClientLoader.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | private string $factory;
21 | private iterable $middlewares;
22 | private array $options;
23 | private ?ClientInterface $loaded = null;
24 |
25 | /**
26 | * @param class-string $factory
27 | */
28 | public function __construct(string $factory, iterable $middlewares, array $options = [])
29 | {
30 | $this->factory = $factory;
31 | $this->middlewares = $middlewares;
32 | $this->options = $options;
33 | }
34 |
35 | public function load(): ClientInterface
36 | {
37 | if (!$this->loaded) {
38 | /** @psalm-suppress DocblockTypeContradiction */
39 | Assert::subclassOf($this->factory, FactoryInterface::class);
40 |
41 | $this->loaded = ($this->factory)::create($this->middlewares, $this->options);
42 | }
43 |
44 | return $this->loaded;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Formatter/FormatterBuilder.php:
--------------------------------------------------------------------------------
1 |
22 | */
23 | private array $decorators = [];
24 |
25 | public static function default(
26 | ): FormatterBuilder {
27 | return new self();
28 | }
29 |
30 | public function withDebug(bool $debug = true): self
31 | {
32 | $this->debug = $debug;
33 |
34 | return $this;
35 | }
36 |
37 | public function withMaxBodyLength(int $maxBodyLength): self
38 | {
39 | $this->maxBodyLength = $maxBodyLength;
40 |
41 | return $this;
42 | }
43 |
44 | /**
45 | * @param Decorator $decorator
46 | */
47 | public function addDecorator(Closure $decorator): self
48 | {
49 | $this->decorators[] = $decorator;
50 |
51 | return $this;
52 | }
53 |
54 | public function build(): Formatter
55 | {
56 | return Fun\pipe(...$this->decorators)(
57 | BasicFormatterFactory::create($this->debug, $this->maxBodyLength)
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Request/Request.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | final class Request implements RequestInterface
17 | {
18 | /**
19 | * @var Method
20 | */
21 | private string $method;
22 | private string $uri;
23 | private array $uriParameters;
24 |
25 | /**
26 | * @var RequestType
27 | */
28 | private $body;
29 |
30 | /**
31 | * @param Method $method
32 | * @param RequestType $body
33 | */
34 | public function __construct(string $method, string $uri, array $uriParameters, $body)
35 | {
36 | $this->method = $method;
37 | $this->uri = $uri;
38 | $this->uriParameters = $uriParameters;
39 | $this->body = $body;
40 | }
41 |
42 | /**
43 | * @return Method
44 | */
45 | public function method(): string
46 | {
47 | return $this->method;
48 | }
49 |
50 | public function uri(): string
51 | {
52 | return $this->uri;
53 | }
54 |
55 | public function uriParameters(): array
56 | {
57 | return $this->uriParameters;
58 | }
59 |
60 | /**
61 | * @return RequestType
62 | */
63 | public function body()
64 | {
65 | return $this->body;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Transport/EncodedTransportFactory.php:
--------------------------------------------------------------------------------
1 | $encoder
22 | * @param DecoderInterface $decoder
23 | *
24 | * @return TransportInterface
25 | */
26 | public static function create(
27 | ClientInterface $client,
28 | UriBuilderInterface $uriBuilder,
29 | EncoderInterface $encoder,
30 | DecoderInterface $decoder,
31 | ): TransportInterface {
32 | /** @var CallbackTransport $transport */
33 | $transport = new CallbackTransport(
34 | EncodingRequestConverter::createWithAutodiscoveredPsrFactories($uriBuilder, $encoder),
35 | static fn (PsrRequestInterface $request) => $client->sendRequest($request),
36 | static fn (ResponseInterface $response) => $decoder($response)
37 | );
38 |
39 | return $transport;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Encoding/Mime/MultiPartEncoder.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | final class MultiPartEncoder implements EncoderInterface
21 | {
22 | private StreamFactoryInterface $streamFactory;
23 |
24 | public function __construct(StreamFactoryInterface $streamFactory)
25 | {
26 | SymfonyMimeDependency::guard();
27 |
28 | $this->streamFactory = $streamFactory;
29 | }
30 |
31 | public static function createWithAutodiscoveredPsrFactories(): self
32 | {
33 | return new self(
34 | Psr17FactoryDiscovery::findStreamFactory()
35 | );
36 | }
37 |
38 | /**
39 | * @param MultiPart $data
40 | */
41 | public function __invoke(RequestInterface $request, $data): RequestInterface
42 | {
43 | return $request
44 | ->withAddedHeader(
45 | 'Content-Type',
46 | string()->assert($data->getPreparedHeaders()->get('content-type')?->getBodyAsString())
47 | )
48 | ->withBody($this->streamFactory->createStream(
49 | $data->bodyToString()
50 | ));
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Transport/CallbackTransport.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | final class CallbackTransport implements TransportInterface
18 | {
19 | /**
20 | * @var callable(RequestInterface): PsrRequestInterface
21 | */
22 | private $requestConverter;
23 |
24 | /**
25 | * @var callable(PsrResponseInterface): ResponseType
26 | */
27 | private $responseConverter;
28 |
29 | /**
30 | * @var callable(PsrRequestInterface): PsrResponseInterface
31 | */
32 | private $sender;
33 |
34 | /**
35 | * @param callable(RequestInterface): PsrRequestInterface $requestConverter
36 | * @param callable(PsrRequestInterface): PsrResponseInterface $sender
37 | * @param callable(PsrResponseInterface): ResponseType $responseConverter
38 | */
39 | public function __construct(
40 | callable $requestConverter,
41 | callable $sender,
42 | callable $responseConverter,
43 | ) {
44 | $this->requestConverter = $requestConverter;
45 | $this->sender = $sender;
46 | $this->responseConverter = $responseConverter;
47 | }
48 |
49 | /**
50 | * @param RequestInterface $request
51 | *
52 | * @return ResponseType
53 | */
54 | public function __invoke(RequestInterface $request)
55 | {
56 | $httpRequest = ($this->requestConverter)($request);
57 | $httpResponse = ($this->sender)($httpRequest);
58 |
59 | return ($this->responseConverter)($httpResponse);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Transport/IO/Input/EncodingRequestConverter.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | final class EncodingRequestConverter implements RequestConverterInterface
20 | {
21 | private RequestFactoryInterface $requestFactory;
22 | private UriBuilderInterface $uriBuilder;
23 | /**
24 | * @var EncoderInterface
25 | */
26 | private EncoderInterface $encoder;
27 |
28 | /**
29 | * @param EncoderInterface $encoder
30 | */
31 | public function __construct(
32 | RequestFactoryInterface $requestFactory,
33 | UriBuilderInterface $uriBuilder,
34 | EncoderInterface $encoder,
35 | ) {
36 | $this->requestFactory = $requestFactory;
37 | $this->encoder = $encoder;
38 | $this->uriBuilder = $uriBuilder;
39 | }
40 |
41 | public static function createWithAutodiscoveredPsrFactories(
42 | UriBuilderInterface $uriBuilder,
43 | EncoderInterface $encoder,
44 | ): self {
45 | return new self(
46 | Psr17FactoryDiscovery::findRequestFactory(),
47 | $uriBuilder,
48 | $encoder
49 | );
50 | }
51 |
52 | public function __invoke(RequestInterface $request): PsrRequestInterface
53 | {
54 | return ($this->encoder)(
55 | $this->requestFactory->createRequest(
56 | $request->method(),
57 | ($this->uriBuilder)($request)
58 | ),
59 | $request->body()
60 | );
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Transport/Presets/BinaryDownloadPreset.php:
--------------------------------------------------------------------------------
1 |
23 | */
24 | public static function create(
25 | ClientInterface $client,
26 | UriBuilderInterface $uriBuilder,
27 | ): TransportInterface {
28 | return self::withEmptyRequest($client, $uriBuilder);
29 | }
30 |
31 | /**
32 | * @return TransportInterface
33 | */
34 | public static function withEmptyRequest(
35 | ClientInterface $client,
36 | UriBuilderInterface $uriBuilder,
37 | ): TransportInterface {
38 | return EncodedTransportFactory::create(
39 | $client,
40 | $uriBuilder,
41 | EmptyBodyEncoder::createWithAutodiscoveredPsrFactories(),
42 | BinaryFileDecoder::createWithAutodiscoveredPsrFactories()
43 | );
44 | }
45 |
46 | /**
47 | * @return TransportInterface
48 | */
49 | public static function withMultiPartRequest(
50 | ClientInterface $client,
51 | UriBuilderInterface $uriBuilder,
52 | ): TransportInterface {
53 | return EncodedTransportFactory::create(
54 | $client,
55 | $uriBuilder,
56 | MultiPartEncoder::createWithAutodiscoveredPsrFactories(),
57 | BinaryFileDecoder::createWithAutodiscoveredPsrFactories()
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Transport/Serializer/SerializerTransport.php:
--------------------------------------------------------------------------------
1 |
17 | */
18 | final class SerializerTransport implements TransportInterface
19 | {
20 | /**
21 | * @var class-string|null
22 | */
23 | private ?string $outputType = null;
24 | private SerializerInterface $serializer;
25 |
26 | /**
27 | * @var TransportInterface
28 | */
29 | private TransportInterface $transport;
30 |
31 | /**
32 | * @param TransportInterface $transport
33 | */
34 | public function __construct(
35 | SerializerInterface $serializer,
36 | TransportInterface $transport,
37 | ) {
38 | $this->transport = $transport;
39 | $this->serializer = $serializer;
40 | }
41 |
42 | /**
43 | * @template NewResponseType of object
44 | *
45 | * @param class-string $output
46 | *
47 | * @return SerializerTransport
48 | */
49 | public function withOutputType(string $output): self
50 | {
51 | /** @var SerializerTransport $new */
52 | $new = new SerializerTransport($this->serializer, $this->transport);
53 | $new->outputType = $output;
54 |
55 | return $new;
56 | }
57 |
58 | public function __invoke(RequestInterface $request)
59 | {
60 | $response = ($this->transport)(
61 | new Request(
62 | $request->method(),
63 | $request->uri(),
64 | $request->uriParameters(),
65 | null === $request->body() ? '' : $this->serializer->serialize($request->body())
66 | )
67 | );
68 |
69 | if (null === $this->outputType) {
70 | return;
71 | }
72 |
73 | return $this->serializer->deserialize($response, $this->outputType);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/examples/sdk.php:
--------------------------------------------------------------------------------
1 |
44 | */
45 | final class UsersResource extends HttpResource
46 | {
47 | use CreateTrait;
48 | use DeleteTrait;
49 | use FindTrait;
50 | use GetTrait;
51 | use PatchTrait;
52 | use UpdateTrait;
53 |
54 | protected function path(): string
55 | {
56 | return '/users';
57 | }
58 | }
59 |
60 | /**
61 | * @template ResultType
62 | */
63 | final class Sdk
64 | {
65 | /**
66 | * @var UsersResource
67 | *
68 | * @psalm-readonly
69 | */
70 | public UsersResource $users;
71 |
72 | /**
73 | * @param TransportInterface $transport
74 | */
75 | public function __construct(TransportInterface $transport)
76 | {
77 | $this->users = new UsersResource($transport);
78 | }
79 | }
80 |
81 | /** @var Sdk $sdk */
82 | $sdk = new Sdk($transport);
83 |
84 | /** @psalm-suppress ForbiddenCode */
85 | var_dump($sdk->users->find('veewee'));
86 |
--------------------------------------------------------------------------------
/src/Encoding/Binary/BinaryFileDecoder.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | final class BinaryFileDecoder implements DecoderInterface
15 | {
16 | /**
17 | * @var callable(ResponseInterface): ?int
18 | */
19 | private $sizeExtractor;
20 |
21 | /**
22 | * @var callable(ResponseInterface): ?string
23 | */
24 | private $mimeTypeExtractor;
25 |
26 | /**
27 | * @var callable(ResponseInterface): ?string
28 | */
29 | private $fileNameExtractor;
30 |
31 | /**
32 | * @var callable(ResponseInterface): ?string
33 | */
34 | private $extensionExtractor;
35 |
36 | /**
37 | * @var callable(ResponseInterface): ?string
38 | */
39 | private $hashExtractor;
40 |
41 | /**
42 | * @param callable(ResponseInterface): ?int $sizeExtractor
43 | * @param callable(ResponseInterface): ?string $mimeTypeExtractor
44 | * @param callable(ResponseInterface): ?string $fileNameExtractor
45 | * @param callable(ResponseInterface): ?string $extensionExtractor
46 | * @param callable(ResponseInterface): ?string $hashExtractor
47 | */
48 | public function __construct(
49 | callable $sizeExtractor,
50 | callable $mimeTypeExtractor,
51 | callable $fileNameExtractor,
52 | callable $extensionExtractor,
53 | callable $hashExtractor,
54 | ) {
55 | $this->sizeExtractor = $sizeExtractor;
56 | $this->mimeTypeExtractor = $mimeTypeExtractor;
57 | $this->fileNameExtractor = $fileNameExtractor;
58 | $this->extensionExtractor = $extensionExtractor;
59 | $this->hashExtractor = $hashExtractor;
60 | }
61 |
62 | public static function createWithAutodiscoveredPsrFactories(): self
63 | {
64 | return new self(
65 | new Extractor\SizeExtractor(),
66 | new Extractor\MimeTypeExtractor(),
67 | new Extractor\FilenameExtractor(),
68 | new Extractor\ExtensionExtractor(),
69 | new Extractor\HashExtractor(Algorithm::Md5),
70 | );
71 | }
72 |
73 | public function __invoke(ResponseInterface $response): BinaryFile
74 | {
75 | return new BinaryFile(
76 | $response->getBody(),
77 | ($this->sizeExtractor)($response),
78 | ($this->mimeTypeExtractor)($response),
79 | ($this->fileNameExtractor)($response),
80 | ($this->extensionExtractor)($response),
81 | ($this->hashExtractor)($response),
82 | );
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Formatter/RemoveSensitiveQueryStringsFormatter.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | private array $sensitiveKeys;
23 |
24 | /**
25 | * @param non-empty-list $sensitiveKeys
26 | */
27 | public function __construct(
28 | HttpFormatter $formatter,
29 | array $sensitiveKeys,
30 | ) {
31 | $this->formatter = $formatter;
32 | $this->sensitiveKeys = $sensitiveKeys;
33 | }
34 |
35 | /**
36 | * @param non-empty-list $sensitiveKeys
37 | *
38 | * @return Decorator
39 | */
40 | public static function createDecorator(array $sensitiveKeys): Closure
41 | {
42 | return static fn (HttpFormatter $formatter): HttpFormatter => new self($formatter, $sensitiveKeys);
43 | }
44 |
45 | public function formatRequest(RequestInterface $request): string
46 | {
47 | return $this->removeQueryStrings($request);
48 | }
49 |
50 | /** @psalm-suppress DeprecatedMethod */
51 | public function formatResponse(ResponseInterface $response): string
52 | {
53 | return $this->formatter->formatResponse($response);
54 | }
55 |
56 | public function formatResponseForRequest(ResponseInterface $response, RequestInterface $request): string
57 | {
58 | if (!method_exists($this->formatter, 'formatResponseForRequest')) {
59 | return $this->formatResponse($response);
60 | }
61 |
62 | /**
63 | * @psalm-suppress UnnecessaryVarAnnotation
64 | *
65 | * @psalm-var string $formatted
66 | */
67 | $formatted = $this->formatter->formatResponseForRequest($response, $request);
68 |
69 | return $formatted;
70 | }
71 |
72 | private function removeQueryStrings(RequestInterface $request): string
73 | {
74 | $uri = $request->getUri();
75 | $query = $uri->getQuery();
76 |
77 | $result = [];
78 | parse_str($query, $result);
79 |
80 | foreach ($this->sensitiveKeys as $key) {
81 | if (!array_key_exists($key, $result)) {
82 | continue;
83 | }
84 |
85 | $result[$key] = 'xxxx';
86 | }
87 |
88 | return $this->formatter->formatRequest(
89 | $request->withUri(
90 | $uri->withQuery(http_build_query($result))
91 | )
92 | );
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/Formatter/RemoveSensitiveHeadersFormatter.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | private array $sensitiveHeaders;
27 |
28 | /**
29 | * @param non-empty-list $sensitiveHeaders
30 | */
31 | public function __construct(HttpFormatter $formatter, array $sensitiveHeaders)
32 | {
33 | $this->formatter = $formatter;
34 | $this->sensitiveHeaders = $sensitiveHeaders;
35 | }
36 |
37 | /**
38 | * @param non-empty-list $sensitiveHeaders
39 | *
40 | * @return Decorator
41 | */
42 | public static function createDecorator(array $sensitiveHeaders): Closure
43 | {
44 | return static fn (HttpFormatter $formatter): HttpFormatter => new self($formatter, $sensitiveHeaders);
45 | }
46 |
47 | public function formatRequest(RequestInterface $request): string
48 | {
49 | return $this->removeCredentials(
50 | $this->formatter->formatRequest($request)
51 | );
52 | }
53 |
54 | /** @psalm-suppress DeprecatedMethod */
55 | public function formatResponse(ResponseInterface $response): string
56 | {
57 | return $this->removeCredentials(
58 | $this->formatter->formatResponse($response)
59 | );
60 | }
61 |
62 | public function formatResponseForRequest(ResponseInterface $response, RequestInterface $request): string
63 | {
64 | if (!method_exists($this->formatter, 'formatResponseForRequest')) {
65 | return $this->formatResponse($response);
66 | }
67 |
68 | /**
69 | * @psalm-suppress UnnecessaryVarAnnotation
70 | *
71 | * @psalm-var string $formatted
72 | */
73 | $formatted = $this->formatter->formatResponseForRequest($response, $request);
74 |
75 | return $this->removeCredentials($formatted);
76 | }
77 |
78 | private function removeCredentials(string $info): string
79 | {
80 | return array_reduce(
81 | $this->sensitiveHeaders,
82 | /** @psalm-suppress InvalidReturnStatement, InvalidReturnType */
83 | fn (string $sensitiveData, string $header): string => Regex\replace(
84 | $sensitiveData,
85 | '{^('.preg_quote($header, '{').')\:(.*)}im',
86 | '$1: xxxx',
87 | ),
88 | $info
89 | );
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Formatter/RemoveSensitiveJsonKeysFormatter.php:
--------------------------------------------------------------------------------
1 |
25 | */
26 | private array $sensitiveJsonKeys;
27 |
28 | /**
29 | * @param non-empty-list $sensitiveJsonKeys
30 | */
31 | public function __construct(HttpFormatter $formatter, array $sensitiveJsonKeys)
32 | {
33 | $this->formatter = $formatter;
34 | $this->sensitiveJsonKeys = $sensitiveJsonKeys;
35 | }
36 |
37 | /**
38 | * @param non-empty-list $sensitiveJsonKeys
39 | *
40 | * @return Decorator
41 | */
42 | public static function createDecorator(array $sensitiveJsonKeys): Closure
43 | {
44 | return static fn (HttpFormatter $formatter): HttpFormatter => new self($formatter, $sensitiveJsonKeys);
45 | }
46 |
47 | public function formatRequest(RequestInterface $request): string
48 | {
49 | return $this->removeCredentials(
50 | $this->formatter->formatRequest($request)
51 | );
52 | }
53 |
54 | /** @psalm-suppress DeprecatedMethod */
55 | public function formatResponse(ResponseInterface $response): string
56 | {
57 | return $this->removeCredentials(
58 | $this->formatter->formatResponse($response)
59 | );
60 | }
61 |
62 | public function formatResponseForRequest(ResponseInterface $response, RequestInterface $request): string
63 | {
64 | if (!method_exists($this->formatter, 'formatResponseForRequest')) {
65 | return $this->formatResponse($response);
66 | }
67 |
68 | /**
69 | * @psalm-suppress UnnecessaryVarAnnotation
70 | *
71 | * @psalm-var string $formatted
72 | */
73 | $formatted = $this->formatter->formatResponseForRequest($response, $request);
74 |
75 | return $this->removeCredentials($formatted);
76 | }
77 |
78 | private function removeCredentials(string $info): string
79 | {
80 | return array_reduce(
81 | $this->sensitiveJsonKeys,
82 | /** @psalm-suppress InvalidReturnStatement, InvalidReturnType */
83 | fn (string $sensitiveData, string $jsonKey): string => Regex\replace(
84 | $sensitiveData,
85 | '{"('.preg_quote($jsonKey, '{').')"\:\s*"([^"]*)"}i',
86 | '"$1": "xxxx"',
87 | ),
88 | $info
89 | );
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/docs/sdk.md:
--------------------------------------------------------------------------------
1 | # Creating a Software Development kit for API's
2 |
3 | If your API is very straight forward, you might not want to create request handlers for every action.
4 | You could for example create a more classic API with this package as well.
5 |
6 | We provide some tools to compose straight-forward Rest HTTP resources.
7 | Here is an example for a REST service:
8 |
9 | ```php
10 | use Phpro\HttpTools\Sdk\HttpResource;
11 | use Phpro\HttpTools\Sdk\Rest;
12 | use Phpro\HttpTools\Request\Request;
13 |
14 | /**
15 | * @template ResultType
16 | * @extends HttpResource
17 | */
18 | final class UsersResouce extends HttpResource
19 | {
20 | use Rest\CreateTrait;
21 | use Rest\FindTrait;
22 | use Rest\GetTrait;
23 | use Rest\UpdateTrait;
24 | use Rest\PatchTrait;
25 | use Rest\DeleteTrait;
26 |
27 | protected function path(): string
28 | {
29 | return '/users';
30 | }
31 |
32 | public function me()
33 | {
34 | $request = new Request('GET', '/user', [], null);
35 | return $this->transport()($request);
36 | }
37 | }
38 | ```
39 |
40 | You could wrap multiple resources in an SDK client like this:
41 |
42 | ```php
43 |
44 | use Phpro\HttpTools\Transport\TransportInterface;
45 |
46 | /**
47 | * @template ResultType
48 | */
49 | final class MyClient
50 | {
51 | /**
52 | * @var UsersResource
53 | * @psalm-readonly
54 | */
55 | public $users;
56 |
57 | /**
58 | * @param TransportInterface $transport
59 | */
60 | public function __construct(TransportInterface $transport)
61 | {
62 | $this->users = new UsersResource($transport);
63 | }
64 | }
65 | ```
66 |
67 | Example usage:
68 |
69 | ```php
70 | /** @var TransportInterface $transport */
71 | /** @var MyClient $sdk */
72 | $sdk = new MyClient($transport);
73 |
74 | var_dump($sdk->users->find('veewee'));
75 | ```
76 |
77 | You could even swap the transport with one that supports a different output type and still keep type-safety!
78 |
79 | > **Note**: If you are using the serializer transport, you might want to set the output types from within the Client.
80 | > You might also want to create a separate function for delete and find.
81 |
82 |
83 | # Built-in traits:
84 |
85 | These are some very opinionated traits that can be used to build your SDK.
86 | If you want to change something for your specific case, you can implement your own method or trait.
87 |
88 | | Trait | Description |
89 | | --- | --- |
90 | | CreateTrait | Can be used to create a new resource from an array |
91 | | DeleteTrait | Can be used to delete a resource from an identifier |
92 | | FindTrait | Can be used to find a resource with an identifier |
93 | | GetTrait | Can be used to list all resources |
94 | | PatchTrait | Can be used to patch a resource with an identifier from an array |
95 | | UpdateTrait | Can be used to update a resource with an identifier from an array |
96 |
97 |
--------------------------------------------------------------------------------
/docs/files.md:
--------------------------------------------------------------------------------
1 | # Dealing with files
2 |
3 | ## Download file
4 |
5 | For downloading files, you can use the `BinaryDecoder` - which has its own `BinaryDownloadPreset`:
6 |
7 | ```php
8 | use Phpro\HttpTools\Encoding\Binary\BinaryFile;
9 | use Phpro\HttpTools\Request\Request;
10 | use Phpro\HttpTools\Transport\Presets\BinaryDownloadPreset;
11 |
12 | $transport = BinaryDownloadPreset::create($client, $uriBuilder);
13 | $binaryFile = $transport(
14 | new Request('GET', '/some/file', [], null),
15 | );
16 |
17 | assert($binaryFile instanceof BinaryFile);
18 | var_dump(
19 | $binaryFile->stream(),
20 | $binaryFile->fileSizeInBytes(),
21 | $binaryFile->hash(),
22 | $binaryFile->extension(),
23 | $binaryFile->mimeType(),
24 | $binaryFile->fileName(),
25 | );
26 | ```
27 |
28 | This decoder will try to parse some information from the Response headers and stream:
29 |
30 | * The stream body can be used to tell the size in bytes of the response or to calculate an MD5 (or other algorithm's) hash.
31 | * `Content-Type`: could be used to guess the mime-type
32 | * `Content-Disposistion`: could result in guessing the filename, extension and/or mime-type.
33 | * `Content-Length`: could be used to guess the size in bytes of the response.
34 |
35 | The decoder is highly configurable, meaning you can alter the way it guesses specific properties or opt-out on intenser logic like calculating a md5 hash.
36 |
37 |
38 | ## Upload file(s)
39 |
40 | If you have to upload files to an API server, you could use the `MultiPartEncoder` which uses `symfony/mime` internally.
41 | An example setup could look like this:
42 |
43 | ```php
44 | use Phpro\HttpTools\Encoding\Json\JsonDecoder;
45 | use Phpro\HttpTools\Encoding\Mime\MultiPartEncoder;
46 | use Phpro\HttpTools\Transport\EncodedTransportFactory;
47 | use Symfony\Component\Mime\Part\DataPart;
48 | use Symfony\Component\Mime\Part\Multipart\FormDataPart;
49 | use Phpro\HttpTools\Request\Request;
50 |
51 | $transport = EncodedTransportFactory::create(
52 | $client,
53 | $uriBuilder,
54 | MultiPartEncoder::createWithAutodiscoveredPsrFactories(),
55 | JsonDecoder::createWithAutodiscoveredPsrFactories()
56 | );
57 |
58 | $jsonData = $transport(
59 | new Request('POST', '/some/file', [], new FormDataPart([
60 | 'name' => 'Jos bos',
61 | 'profile-pic' => DataPart::fromPath('/my-profile-pic.jpg')
62 | ])),
63 | );
64 | ```
65 |
66 | **Note:** If you wish not to use `symfony/mime` for uploading files, you could use `guzzle/psr7`'s `MultipartStream` with the existing `StreamEncoder` option:
67 |
68 | ```php
69 | use GuzzleHttp\Psr7\MultipartStream;
70 | use Phpro\HttpTools\Encoding\Json\JsonDecoder;
71 | use Phpro\HttpTools\Encoding\Stream\StreamEncoder;
72 | use Phpro\HttpTools\Transport\EncodedTransportFactory;
73 | use Symfony\Component\Mime\Part\DataPart;
74 | use Symfony\Component\Mime\Part\Multipart\FormDataPart;
75 | use Phpro\HttpTools\Request\Request;
76 |
77 | $transport = EncodedTransportFactory::create(
78 | $client,
79 | $uriBuilder,
80 | StreamEncoder::createWithAutodiscoveredPsrFactories(),
81 | JsonDecoder::createWithAutodiscoveredPsrFactories()
82 | );
83 |
84 | $multiPartStream = new MultipartStream($parts)
85 | $jsonData = $transport(
86 | new Request('POST', '/some/file', [], $multiPartStream)),
87 | );
88 | ```
89 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "phpro/http-tools",
3 | "description": "HTTP tools for developing more consistent HTTP implementations.",
4 | "keywords": ["PSR-7", "PSR-18", "PSR-17", "HTTP", "Client"],
5 | "type": "library",
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Toon Verwerft",
10 | "email": "toon.verwerft@phpro.be"
11 | },
12 | {
13 | "name": "Jelle Deneweth",
14 | "email": "jelle.deneweth@gmail.com"
15 | }
16 | ],
17 | "require": {
18 | "php": "~8.3.0 || ~8.4.0 || ~8.5.0",
19 | "ext-json": "*",
20 | "azjezz/psl": "^3.1 || ^4.0",
21 | "cardinalby/content-disposition": "^1.1",
22 | "league/uri": "^7.3",
23 | "php-http/client-common": "^2.7",
24 | "php-http/discovery": "^1.19",
25 | "php-http/httplug": "^2.4",
26 | "php-http/logger-plugin": "^1.3",
27 | "php-http/message": "^1.16 || ^2.0",
28 | "psr/http-client-implementation": "^1.0",
29 | "psr/http-factory": "^1.0",
30 | "psr/http-factory-implementation": "^1.0",
31 | "psr/http-message": "^1.0 || ^2.0",
32 | "psr/http-message-implementation": "^1.0",
33 | "psr/log": "^3",
34 | "webmozart/assert": "^1.11"
35 | },
36 | "require-dev": {
37 | "guzzlehttp/guzzle": "^7.8",
38 | "nyholm/psr7": "^1.8",
39 | "php-cs-fixer/shim": "~3.88",
40 | "php-http/message-factory": "1.1.0",
41 | "php-http/mock-client": "^1.6",
42 | "php-http/vcr-plugin": "^1.2",
43 | "phpro/grumphp-shim": "^2.17",
44 | "phpunit/phpunit": "~12.4",
45 | "symfony/http-client": "^5.4.26 || ^6.0 || ^7.0",
46 | "symfony/mime": "^6.0 || ^7.0",
47 | "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0",
48 | "symfony/property-access": "^5.4 || ^6.0 || ^7.0",
49 | "symfony/serializer": "^5.4 || ^6.0 || ^7.0",
50 | "vimeo/psalm": "~6.13"
51 | },
52 | "suggest": {
53 | "symfony/http-client": "If you want to use the built-in symfony/http-client tools.",
54 | "symfony/serializer": "If you want to use symfony serializer to handle request serialization and response deserialization.",
55 | "symfony/mime": "If you want to use symfony/mime to upload or download binary files.",
56 | "guzzlehttp/guzzle": "If you want to use the built-in guzzlehttp/guzzle tools.",
57 | "php-http/mock-client": "For testing HTTP clients through mocking Requests and responses.",
58 | "php-http/vcr-plugin": "For testing HTTP clients through storing and replaying requests and responses."
59 | },
60 | "config": {
61 | "sort-packages": true,
62 | "allow-plugins": {
63 | "phpro/grumphp-shim": true,
64 | "php-http/discovery": true
65 | }
66 | },
67 | "autoload": {
68 | "psr-4": {
69 | "Phpro\\HttpTools\\": "src"
70 | }
71 | },
72 | "autoload-dev": {
73 | "psr-4": {
74 | "Phpro\\HttpTools\\Tests\\": "tests"
75 | }
76 | },
77 | "scripts": {
78 | "functional-testserver": [
79 | "Composer\\Config::disableProcessTimeout",
80 | "php -S 127.0.0.1:8000 -t tests/Fixtures/functional/server"
81 | ]
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | setFinder(
5 | PhpCsFixer\Finder::create()
6 | ->in([
7 | __DIR__ . '/examples',
8 | __DIR__ . '/src',
9 | __DIR__ . '/tests',
10 | ])
11 | ->name('*.php')
12 | )
13 | ->setRiskyAllowed(true)
14 | ->setRules([
15 | '@Symfony' => true,
16 | 'align_multiline_comment' => true,
17 | 'array_indentation' => true,
18 | 'array_syntax' => ['syntax' => 'short'],
19 | 'backtick_to_shell_exec' => true,
20 | 'blank_line_before_statement' => ['statements' => ['return']],
21 | 'class_keyword_remove' => false,
22 | 'combine_consecutive_issets' => true,
23 | 'combine_consecutive_unsets' => true,
24 | 'comment_to_phpdoc' => true,
25 | 'compact_nullable_typehint' => true,
26 | 'date_time_immutable' => true,
27 | 'declare_strict_types' => true,
28 | 'doctrine_annotation_array_assignment' => true,
29 | 'doctrine_annotation_braces' => true,
30 | 'doctrine_annotation_indentation' => true,
31 | 'doctrine_annotation_spaces' => true,
32 | 'escape_implicit_backslashes' => true,
33 | 'explicit_indirect_variable' => true,
34 | 'explicit_string_variable' => true,
35 | 'final_internal_class' => true,
36 | 'final_class' => true,
37 | 'fully_qualified_strict_types' => true,
38 | 'general_phpdoc_annotation_remove' => false,
39 | 'header_comment' => false,
40 | 'heredoc_to_nowdoc' => false,
41 | 'linebreak_after_opening_tag' => true,
42 | 'list_syntax' => ['syntax' => 'short'],
43 | 'mb_str_functions' => true,
44 | 'method_chaining_indentation' => true,
45 | 'multiline_comment_opening_closing' => true,
46 | 'multiline_whitespace_before_semicolons' => true,
47 | 'native_function_invocation' => false,
48 | 'no_alternative_syntax' => true,
49 | 'no_blank_lines_before_namespace' => false,
50 | 'no_null_property_initialization' => true,
51 | 'no_php4_constructor' => true,
52 | 'echo_tag_syntax' => ['format'=> 'long'],
53 | 'no_superfluous_elseif' => true,
54 | 'no_unreachable_default_argument_value' => true,
55 | 'no_useless_else' => true,
56 | 'no_useless_return' => true,
57 | 'not_operator_with_space' => false,
58 | 'not_operator_with_successor_space' => false,
59 | 'ordered_class_elements' => false,
60 | 'ordered_imports' => true,
61 | 'php_unit_dedicate_assert' => false,
62 | 'php_unit_expectation' => false,
63 | 'php_unit_mock' => false,
64 | 'php_unit_namespaced' => false,
65 | 'php_unit_no_expectation_annotation' => false,
66 | 'phpdoc_order_by_value' => ['annotations' => ['covers']],
67 | 'php_unit_set_up_tear_down_visibility' => true,
68 | 'php_unit_strict' => false,
69 | 'php_unit_test_annotation' => false,
70 | 'php_unit_test_class_requires_covers' => false,
71 | 'php_unit_method_casing' => ['case' => 'snake_case'],
72 | 'phpdoc_add_missing_param_annotation' => true,
73 | 'phpdoc_order' => true,
74 | 'phpdoc_types_order' => ['null_adjustment' => 'always_last'],
75 | 'pow_to_exponentiation' => true,
76 | 'psr_autoloading' => ['dir' => 'src'],
77 | 'random_api_migration' => false,
78 | 'simplified_null_return' => true,
79 | 'static_lambda' => false,
80 | 'strict_comparison' => true,
81 | 'strict_param' => true,
82 | 'string_line_ending' => true,
83 | 'ternary_to_null_coalescing' => true,
84 | 'void_return' => true,
85 | 'yoda_style' => true,
86 | 'single_line_throw' => false,
87 | 'phpdoc_align' => ['align' => 'left'],
88 | 'phpdoc_to_comment' => false,
89 | 'global_namespace_import' => [
90 | 'import_classes' => true,
91 | 'import_constants' => true,
92 | 'import_functions' => true,
93 | ],
94 | ])
95 | ;
96 |
--------------------------------------------------------------------------------
/src/Client/ClientBuilder.php:
--------------------------------------------------------------------------------
1 |
42 | */
43 | private SplPriorityQueue $plugins;
44 | /**
45 | * @var list
46 | */
47 | private array $decorators = [];
48 |
49 | /**
50 | * @param iterable $middlewares
51 | */
52 | public function __construct(
53 | ?ClientInterface $client = null,
54 | iterable $middlewares = [],
55 | ) {
56 | $this->client = $client ?? Psr18ClientDiscovery::find();
57 | /** @var SplPriorityQueue $plugins */
58 | $plugins = new SplPriorityQueue();
59 | $this->plugins = $plugins;
60 |
61 | foreach ($middlewares as $middleware) {
62 | $plugins->insert($middleware, self::PRIORITY_LEVEL_DEFAULT);
63 | }
64 | }
65 |
66 | public static function default(
67 | ?ClientInterface $client = null,
68 | ): self {
69 | return new self($client, [
70 | new Plugin\ErrorPlugin(),
71 | ]);
72 | }
73 |
74 | /**
75 | * @param Decorator $decorator
76 | */
77 | public function addDecorator(Closure $decorator): self
78 | {
79 | $this->decorators[] = $decorator;
80 |
81 | return $this;
82 | }
83 |
84 | public function addPlugin(
85 | Plugin $plugin,
86 | int $priority = self::PRIORITY_LEVEL_DEFAULT,
87 | ): self {
88 | $this->plugins->insert($plugin, $priority);
89 |
90 | return $this;
91 | }
92 |
93 | /**
94 | * @param Closure(ClientInterface): Plugin $pluginBuilder
95 | */
96 | public function addPluginWithCurrentlyConfiguredClient(
97 | Closure $pluginBuilder,
98 | int $priority = self::PRIORITY_LEVEL_DEFAULT,
99 | ): self {
100 | return $this->addPlugin(
101 | $pluginBuilder($this->build()),
102 | $priority,
103 | );
104 | }
105 |
106 | public function addAuthentication(
107 | Authentication $authentication,
108 | int $priority = self::PRIORITY_LEVEL_SECURITY,
109 | ): self {
110 | return $this->addPlugin(new Plugin\AuthenticationPlugin($authentication), $priority);
111 | }
112 |
113 | public function addLogger(
114 | LoggerInterface $logger,
115 | ?Formatter $formatter = null,
116 | int $priority = self::PRIORITY_LEVEL_LOGGING,
117 | ): self {
118 | return $this->addPlugin(new Plugin\LoggerPlugin($logger, $formatter), $priority);
119 | }
120 |
121 | /**
122 | * @param array $headers
123 | *
124 | * @return $this
125 | */
126 | public function addHeaders(
127 | array $headers,
128 | int $priority = self::PRIORITY_LEVEL_DEFAULT,
129 | ): self {
130 | return $this->addPlugin(new Plugin\HeaderSetPlugin($headers), $priority);
131 | }
132 |
133 | public function addBaseUri(
134 | UriInterface|string $baseUri,
135 | bool $replaceHost = true,
136 | int $priority = self::PRIORITY_LEVEL_DEFAULT,
137 | ): self {
138 | $baseUri = match (true) {
139 | is_string($baseUri) => Psr17FactoryDiscovery::findUriFactory()->createUri($baseUri),
140 | default => $baseUri,
141 | };
142 |
143 | return $this->addPlugin(new Plugin\BaseUriPlugin($baseUri, ['replace' => $replaceHost]), $priority);
144 | }
145 |
146 | public function addRecording(
147 | NamingStrategyInterface $namingStrategy,
148 | RecorderInterface&PlayerInterface $recorder,
149 | int $priority = self::PRIORITY_LEVEL_LOGGING,
150 | ): self {
151 | return $this
152 | ->addPlugin(new RecordPlugin($namingStrategy, $recorder), $priority)
153 | ->addPlugin(new ReplayPlugin($namingStrategy, $recorder, false), $priority);
154 | }
155 |
156 | /**
157 | * @param PluginCallback $callback
158 | */
159 | public function addCallback(callable $callback, int $priority = self::PRIORITY_LEVEL_DEFAULT): self
160 | {
161 | return $this->addPlugin(new CallbackPlugin($callback), $priority);
162 | }
163 |
164 | public function build(): PluginClient
165 | {
166 | return PluginsConfigurator::configure(
167 | Fun\pipe(...$this->decorators)($this->client),
168 | [...clone $this->plugins]
169 | );
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/docs/transports.md:
--------------------------------------------------------------------------------
1 | # Transports
2 |
3 | Transports are responsible for transforming a Request model into a PSR-7 HTTP request and asks a response through the actual HTTP client.
4 | Since the data inside the request and response value objects is set-up in a flexible way, we also provide a lot of ways of transforming the value objects into PSR-7 HTTP Requests and Responses.
5 |
6 | This is done by providing an encoding layer.
7 |
8 | ## Encoding
9 |
10 | Encoding is split up into `Encoder` and `Decoder` classes.
11 | The Encoder classes are responsible for converting the body of a Request value object into the body payload of a PSR-7 Http request.
12 | The Decoder classes are responsible for converting the payload of a PSR-7 Response object into a result that can be used by the transport or request handler.
13 |
14 | By splitting the Encoding into 2 components, you can compose any encoding component in a transport.
15 | Examples:
16 |
17 |
18 | | Transport | Encoder | Decoder |
19 | | --- | --- | --- |
20 | | `TransportInterface` | `JsonEncoder` | `JsonDecoder` |
21 | | `TransportInterface` | `RawEncoder` | `RawStringEncoder` |
22 | | `TransportInterface` | `JsonEncoder` | `RawStringEncoder` |
23 | | `TransportInterface` | `EmptyBodyEncoder` | `RawStringEncoder` |
24 |
25 |
26 | ### Built-in encodings
27 |
28 | This package contains some frequently used encoders / decoders for you:
29 |
30 | | Class | EncodingType | Action |
31 | |-------------------------|---------------------------------------|-------------------------------------------------------------------------------------|
32 | | `EmptyBodyEncoder` | `EncoderInterface` | Creates empty request body |
33 | | `BinaryFileDecoder` | `DecoderInterface` | Parses file information from the HTTP response and returns a `BinaryFile` DTO |
34 | | `FormUrlencodedEncoder` | `EncoderInterface` | Adds form urlencoded body and headers to request |
35 | | `FormUrlencodedDecoder` | `DecoderInterface` | Converts form urlencoded response body to array |
36 | | `JsonEncoder` | `EncoderInterface` | Adds json body and headers to request |
37 | | `JsonDecoder` | `DecoderInterface` | Converts json response body to array |
38 | | `MultiPartEncoder` | `EncoderInterface` | Adds symfony/mime `AbstractMultipartPart`as HTTP body. Handy for form data + files. |
39 | | `StreamEncoder` | `EncoderInterface` | Adds PSR-7 Stream as request body |
40 | | `StreamDecoder` | `DecoderInterface` | Returns the PSR-7 Stream as response result |
41 | | `RawEncoder` | `EncoderInterface` | Adds raw string as request body |
42 | | `RawDecoder` | `DecoderInterface` | Returns the raw PSR-7 body string as response result |
43 | | `ResponseDecoder` | `DecoderInterface` | Returns the received PSR-7 response as result |
44 |
45 | ## Built-in transport presets:
46 |
47 | We've composed some of the encodings above into pre-configured transports:
48 |
49 |
50 | | Preset | RequestType | ResponseType | Factory method |
51 | |------------------------|-------------------------|---------------------|------------------------|
52 | | `BinaryDownloadPreset` | `null` | `BinaryFile` | `withEmptyRequest` |
53 | | `BinaryDownloadPreset` | `AbstractMultipartPart` | `BinaryFile` | `withMultiPartRequest` |
54 | | `FormUrlencodedPreset` | `?array` | `array` | `create` |
55 | | `JsonPreset` | `?array` | `array` | `create` |
56 | | `PsrPreset` | `string` | `ResponseInterface` | `create` |
57 | | `RawPreset` | `string` | `string` | `create` |
58 |
59 | ## Creating your own configuration
60 |
61 | We provide an `EncodedTransport` class that helps you build your own configuration.
62 | This transport takes a configurable encoder and decoder:
63 |
64 |
65 | ```php
66 | use Phpro\HttpTools\Transport\EncodedTransportFactory;
67 |
68 | EncodedTransportFactory::create(
69 | $client,
70 | $uriBuilder,
71 | $encoder,
72 | $decoder
73 | );
74 | ```
75 |
76 | ## Other transports
77 |
78 | ### Files
79 |
80 | We provide encoders and decoders for uploading and downloading files.
81 |
82 | [You can learn more about this on this dedicated page.](./files.md)
83 |
84 | ### SerializerTransport
85 |
86 | This transport allows you to use an external serializer to handle request serialization and response deserialization.
87 | You can use the symfony/serializer component or any other serializer you please.
88 |
89 | However, you do need to specify what output type the transport will deserialize to. (e.g. inside a request handler)
90 |
91 | ```php
92 | use Phpro\HttpTools\Serializer\SymfonySerializer;
93 | use Phpro\HttpTools\Transport\Presets\RawPreset;
94 | use Phpro\HttpTools\Transport\Serializer\SerializerTransport;
95 |
96 | $transport = new SerializerTransport(
97 | new SymfonySerializer($theSymfonySerializer, 'json'),
98 | RawPreset::create($client, $uriBuilder)
99 | );
100 |
101 | $transport->withOutputType(SomeResponse::class);
102 | ```
103 |
104 | If you want to use symfony/validator, you might need to:
105 |
106 | ```bash
107 | composer require symfony/serializer
108 | ```
109 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | [](https://packagist.org/packages/phpro/http-tools/stats)
3 | [](https://packagist.org/packages/phpro/http-tools)
4 |
5 |
6 | # HTTP-Tools
7 |
8 | The goal of this package is to provide you some tools to set-up a data-centric, consistent HTTP integration.
9 | The HTTP client implementation you want to use is just a small implementation detail and doesn't matter.
10 | However, here are some default guidelines:
11 |
12 | ## Prerequisites
13 |
14 | Choosing what HTTP package you want to us is all up to you.
15 | We do require a PSR implementations in order to install this package:
16 |
17 | * PSR-7: `psr/http-message-implementation` like `nyholm/psr7` or `guzzlehttp/psr7`
18 | * PSR-17: `psr/http-factory-implementation` like `nyholm/psr7` or `guzzlehttp/psr7`
19 | * PSR-18: `psr/http-client-implementation` like `symfony/http-client` or `guzzlehttp/guzzle`
20 |
21 | ## Installation
22 |
23 | ```bash
24 | composer require phpro/http-tools
25 | ```
26 |
27 | ## Setting up an HTTP client :
28 |
29 | You can choose whatever HTTP client you want.
30 | However, this package provides some convenient factories that make the configuration a bit easier.
31 |
32 | The factory accepts a list of implementation specific plugins / middlewares.
33 | Besides that, you can configure implementation-specific options like a base_uri or default headers.
34 |
35 | ```php
36 | $_ENV['SOME_CLIENT_BASE_URI']];
44 |
45 | $httpClient = AutoDiscoveredClientFactory::create($middlewares);
46 | $httpClient = GuzzleClientFactory::create($guzzlePlugins, $options);
47 | $httpClient = SymfonyClientFactory::create($middlewares, $options);
48 |
49 | // If you are using guzzle, you can both use guzzle and httplug plugins.
50 | // You can wrap additional httplug plugins like this:
51 | $httpClient = PluginsConfigurator::configure($httpClient, $middlewares);
52 | ```
53 |
54 | You can always create your own factory if you want to have more control or want to use another tool!
55 |
56 | **Note:** This package does not download a specific HTTP implementation. You can choose whatever package you want, but you'll have to manually add it to composer.
57 |
58 | ### Configuring the client through plugins
59 |
60 | [Plugin](http://docs.php-http.org/en/latest/plugins/index.html)-[Middleware](http://docs.guzzlephp.org/en/stable/handlers-and-middleware.html) : Patato-Patato.
61 |
62 | If you want to extend how an HTTP client works, we want you to use plugins!
63 | You can use plugins for everything: logging, authentication, language specification, ...
64 |
65 | Examples:
66 |
67 | ```php
68 | addBaseUri('https://www.google.com')
96 | ->addHeaders([
97 | 'x-Foo' => 'bar',
98 | ])
99 | ->addAuthentication(new BasicAuth('user', 'pass'))
100 | ->addPlugin(new YourPlugin(), priority: 99)
101 | // -> and many more ...
102 | ->build();
103 | ```
104 |
105 | Our suggested approach is to create a configuration object for all variable client configurations and services.
106 | This can be used in combination with a PHP factory class that builds your client through this builder:
107 |
108 | ```php
109 | final readonly class YourClientConfig {
110 | public function __construct(
111 | public string $apiUrl,
112 | #[\SensitiveParameter] public string $apiKey,
113 | public LoggerInterface $logger
114 | ) {
115 | }
116 | }
117 | ```
118 |
119 | ```php
120 | final readonly class YourClientFactory
121 | {
122 | public static function create(YourClientConfig $config): ClientInterface
123 | {
124 | return ClientBuilder::default()
125 | ->addBaseUri($config->apiUrl)
126 | ->addHeaders(['x-Api-Key' => $config->apiKey])
127 | ->build();
128 | }
129 | }
130 | ```
131 |
132 | ### Logging
133 |
134 | This package contains the `php-http/logger-plugin`.
135 | On top of that, we've added some decorators that help you strip out sensitive information from the logs.
136 | You can switch from full to simple logging by specifying a debug parameter!
137 |
138 | ```php
139 | addLogger(
174 | new ConsoleLogger(new ConsoleOutput(OutputInterface::VERBOSITY_DEBUG)),
175 | FormatterBuilder::default()
176 | ->withDebug(true)
177 | ->withMaxBodyLength(1000)
178 | ->addDecorator(RemoveSensitiveHeadersFormatter::createDecorator([
179 | 'X-SENSITIVE-HEADER',
180 | ]))
181 | ->build()
182 | )
183 | ->build();
184 | ```
185 |
186 |
187 | [More info ...](http://docs.php-http.org/en/latest/plugins/logger.html)
188 |
189 | ## Using the HTTP-Client
190 |
191 | We don't want you to use the PSR-18 client directly! Instead, we suggest you to use a request handler principle.
192 | So what does this architecture look like?
193 |
194 | 
195 |
196 | * **Models**: Request / Response value objects that can be used as wrapper around the outgoging or incoming data (arrays, objects, strings, ...).
197 | * **RequestHandler**: Transform a request data model into a response data model by using a transport. You could add error handling in there as well.
198 | * **Transport**: Transforms a Request data model into a PSR-7 HTTP request and asks for a response through the actual HTTP client.
199 | * **Encoding**: A transport can take encoders / decoders that are responsible for converting the value objects data to e.g. JSON payloads and visa versa.
200 | * **HTTP-Client**: Whichever PSR-18 HTTP client you want to use: guzzle, curl, symfony/http-client, ...
201 |
202 | [More information on Transports and Encodings](docs/transports.md)
203 |
204 | By using this architecture, we provide an easy to extend flow with models that replace cumbersome array structures.
205 |
206 |
207 | You might be familiar with 1 "client" class that provides access to multiple API endpoints. We see that approach as a multi-requesthandler class.
208 | You are free to choose that approach, owever, we suggest using 1 request handler per API endpoint.
209 | This way, you can inject / mock only the things you require at that moment into your codebase.
210 |
211 | Example implementation:
212 |
213 | ```php
214 |
238 | */
239 | private TransportInterface $transport
240 | ) {}
241 |
242 | public function __invoke(ListRequest $request): ListResponse
243 | {
244 | // You could validate the result first + throw exceptions based on invalid content
245 | // Tip : never trust APIs!
246 | // Try to gracefully fall back if possible and keep an eye on how the implementation needs to handle errors!
247 |
248 | return ListResponse::fromRawArray(
249 | ($this->transport)($request)
250 | );
251 | }
252 | }
253 | ```
254 |
255 | ```php
256 |
265 | */
266 | class ListRequest implements RequestInterface
267 | {
268 | public function method() : string
269 | {
270 | return 'GET';
271 | }
272 |
273 | public function uri() : string
274 | {
275 | return '/list{?query}';
276 | }
277 |
278 | public function uriParameters() : array
279 | {
280 | return [
281 | 'query' => 'somequery',
282 | ];
283 | }
284 |
285 | public function body() : array
286 | {
287 | return [];
288 | }
289 | }
290 |
291 | // By wrapping the response in a Value Object, you can sanitize and normalize data.
292 | // You could as well lazilly throw an exception in here if some value is missing.
293 | // However, that's might be more of a task for a request-handler.
294 |
295 | class ListResponse
296 | {
297 | public static function fromRawArray(array $data): self
298 | {
299 | return new self($data);
300 | }
301 |
302 | public function getItems(): array
303 | {
304 | // Never trust APIs!
305 | return (array) ($this->data['items'] ?? []);
306 | }
307 | }
308 | ```
309 |
310 | This example is rather easy and might seem like overkill at first.
311 | The true power will be visible once you create multiple named constructors and conditional porperty accessors inside the request models.
312 | The response models, if crafted carefully, will improve the stability of your integration!
313 |
314 | ## Async request handlers
315 |
316 | In order to send async requests, you can use this package in combination with fiber-based PSR-18 clients.
317 | The architecture can remain as is.
318 |
319 | An example client based on ReactPHP might be based on this:
320 |
321 | ```sh
322 | composer require react/async veewee/psr18-react-browser
323 | ```
324 |
325 | *(There currently is no official fiber based PSR-18 implementation of either AMP or ReactPHP. Therefore, [a small bridge can be used intermediately](https://github.com/veewee/psr18-react-browser))*
326 |
327 | Since fibers deal with the async part, you can write your Request handlers is if they were synchronous:
328 |
329 | ```php
330 |
339 | */
340 | private TransportInterface $transport
341 | ) {}
342 |
343 | public function __invoke(FetchRequest $request): Something
344 | {
345 | return Something::tryParse(
346 | ($this->transport)($data)
347 | );
348 | }
349 | }
350 | ```
351 |
352 | In order to fetch multiple simultaneous requests, you can execute these in parallel:
353 |
354 | ```php
355 | use Phpro\HttpTools\Transport\Presets\JsonPreset;
356 | use Phpro\HttpTools\Uri\RawUriBuilder;
357 | use Phpro\HttpTools\Uri\TemplatedUriBuilder;
358 | use Veewee\Psr18ReactBrowser\Psr18ReactBrowserClient;
359 | use function React\Async\async;
360 | use function React\Async\await;
361 | use function React\Async\parallel;
362 |
363 | $client = Psr18ReactBrowserClient::default();
364 | $transport = JsonPreset::create($client, new TemplatedUriBuilder());
365 | $handler = new FetchSomething($transport);
366 |
367 | $run = fn($id) => async(fn () => $handler(new FetchRequest($id)));
368 | $things = await(parallel([
369 | $run(1),
370 | $run(2),
371 | $run(3),
372 | ]));
373 | ```
374 |
375 | If your client is fiber compatible, this will fetch all requests in parallel.
376 | If your client is not fiber compatible, this will result in the requests being performed in series.
377 |
378 | ## SDK
379 |
380 | In some situations, writing request handlers might be overkill.
381 | This package also provides some tools to compose a more generic API client instead.
382 | However, our primary suggestion is to create specific request handlers instead!
383 |
384 | [More information on creating SDKs](docs/sdk.md)
385 |
386 | ## Testing HTTP clients
387 |
388 | This tool provided some traits for unit testing your API client with PHPUnit.
389 |
390 | ### UseHttpFactories
391 |
392 | This trait can help you build requests and responses inside your tests without worrying what HTTP package you use:
393 |
394 | * `createRequest`
395 | * `createResponse`
396 | * `createStream`
397 | * `createEmptyHttpClientException`
398 |
399 | ### UseHttpToolsFactories
400 |
401 | This trait can help you build HTTP tools specific objects in unit tests.
402 | It could be hande to e.g. test transports.
403 |
404 | * `createToolsRequest`
405 |
406 | Example:
407 |
408 | ```php
409 | $request = $this->createToolsRequest('GET', '/some-endpoint', [], ['hello' => 'world']);
410 | ```
411 |
412 | ### UseMockClient
413 |
414 | *Includes `UseHttpFactories` trait*
415 |
416 | Preferably, this one will be used to test your own middlewares and transports.
417 | It is also possible to test a request-handler, but you'll have to manually provide the response for it.
418 |
419 | example:
420 |
421 | ```php
422 | client = $this->mockClient(function (Client $client): Client {
435 | $client->setDefaultException(new \Exception('Dont call me!'));
436 | return $client;
437 | });
438 | }
439 | }
440 | ```
441 |
442 | [More info ...](http://docs.php-http.org/en/latest/clients/mock-client.html)
443 |
444 | ### UseVcrClient
445 |
446 | *Includes `UseHttpFactories` trait*
447 |
448 | This one can be used to test your request-handlers with realtime data.
449 | The first you use it in your test, it will do the actual HTTP request.
450 | The response of this request will be recorded and stored inside your project.
451 | The second time the test runs, it will use the recorded version.
452 |
453 | example:
454 |
455 | ```php
456 | client = AutoDiscoveredClientFactory::create([
470 | ...$this->useRecording(FIXTURES_DIR, new PathNamingStrategy())
471 | ]);
472 | }
473 | }
474 | ```
475 |
476 | [More info ...](http://docs.php-http.org/en/latest/plugins/vcr.html)
477 |
--------------------------------------------------------------------------------