Project Path: /home/ddebowczyk/projects/_php/saloon/src

Source Tree:

```
src
├── Config.php
├── Contracts
│   ├── IntegerStore.php
│   ├── MultipartBodyFactory.php
│   ├── RequestMiddleware.php
│   ├── ArrayStore.php
│   ├── Sender.php
│   ├── DataObjects
│   │   └── WithResponse.php
│   ├── Authenticator.php
│   ├── ResponseMiddleware.php
│   └── Body
│       ├── BodyRepository.php
│       ├── HasBody.php
│       └── MergeableBody.php
├── Enums
│   ├── PipeOrder.php
│   └── Method.php
├── Helpers
│   ├── Helpers.php
│   ├── ObjectHelpers.php
│   ├── StringHelpers.php
│   ├── URLHelper.php
│   ├── RequestExceptionHelper.php
│   ├── Storage.php
│   ├── StatusCodeHelper.php
│   ├── MiddlewarePipeline.php
│   ├── Pipeline.php
│   ├── ArrayHelpers.php
│   └── Debugger.php
├── Traits
│   ├── Bootable.php
│   ├── Conditionable.php
│   ├── Plugins
│   │   ├── AlwaysThrowOnErrors.php
│   │   ├── AcceptsJson.php
│   │   └── HasTimeout.php
│   ├── Auth
│   │   ├── AuthenticatesRequests.php
│   │   └── RequiresAuth.php
│   ├── PendingRequest
│   │   └── ManagesPsrRequests.php
│   ├── Macroable.php
│   ├── Makeable.php
│   ├── HasDebugging.php
│   ├── HandlesPsrRequest.php
│   ├── ManagesExceptions.php
│   ├── Connector
│   │   ├── SendsRequests.php
│   │   └── HasSender.php
│   ├── Body
│   │   ├── CreatesStreamFromString.php
│   │   ├── HasXmlBody.php
│   │   ├── HasStringBody.php
│   │   ├── HasFormBody.php
│   │   ├── ChecksForHasBody.php
│   │   ├── HasJsonBody.php
│   │   ├── HasMultipartBody.php
│   │   └── HasStreamBody.php
│   ├── RequestProperties
│   │   ├── HasConfig.php
│   │   ├── HasRequestProperties.php
│   │   ├── HasMiddleware.php
│   │   ├── HasTries.php
│   │   ├── HasDelay.php
│   │   ├── HasHeaders.php
│   │   └── HasQuery.php
│   ├── Responses
│   │   ├── HasCustomResponses.php
│   │   └── HasResponse.php
│   └── Request
│       ├── CreatesDtoFromResponse.php
│       └── HasConnector.php
├── Http
│   ├── Middleware
│   │   ├── ValidateProperties.php
│   │   └── DelayMiddleware.php
│   ├── Connector.php
│   ├── PendingRequest.php
│   ├── Request.php
│   ├── Senders
│   │   ├── GuzzleSender.php
│   │   └── Factories
│   │       └── GuzzleMultipartBodyFactory.php
│   ├── Auth
│   │   ├── DigestAuthenticator.php
│   │   ├── NullAuthenticator.php
│   │   ├── CertificateAuthenticator.php
│   │   ├── HeaderAuthenticator.php
│   │   ├── MultiAuthenticator.php
│   │   ├── AccessTokenAuthenticator.php
│   │   ├── TokenAuthenticator.php
│   │   ├── QueryAuthenticator.php
│   │   └── BasicAuthenticator.php
│   ├── PendingRequest
│   │   ├── AuthenticatePendingRequest.php
│   │   ├── BootConnectorAndRequest.php
│   │   ├── MergeBody.php
│   │   ├── MergeDelay.php
│   │   ├── MergeRequestProperties.php
│   │   └── BootPlugins.php
│   ├── SoloRequest.php
│   ├── Pool.php
│   ├── Connectors
│   │   └── NullConnector.php
│   ├── BaseResource.php
│   ├── Response.php
├── Data
│   ├── FactoryCollection.php
│   ├── RecordedResponse.php
│   ├── MultipartValue.php
│   └── Pipe.php
└── Repositories
    ├── IntegerStore.php
    ├── ArrayStore.php
    └── Body
        ├── FormBodyRepository.php
        ├── ArrayBodyRepository.php
        ├── StringBodyRepository.php
        ├── JsonBodyRepository.php
        ├── StreamBodyRepository.php
        └── MultipartBodyRepository.php

```


`/home/ddebowczyk/projects/_php/saloon/src/Config.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon;

use Saloon\Enums\PipeOrder;
use Saloon\Contracts\Sender;
use Saloon\Http\PendingRequest;
use Saloon\Http\Senders\GuzzleSender;
use Saloon\Helpers\MiddlewarePipeline;
use Saloon\Exceptions\StrayRequestException;

final class Config
{
    /**
     * Default Sender
     *
     * @var class-string<\Saloon\Contracts\Sender>
     */
    public static string $defaultSender = GuzzleSender::class;

    /**
     * Default TLS Method (v1.2)
     */
    public static int $defaultTlsMethod = STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;

    /**
     * Default timeout (in seconds) for establishing a connection.
     */
    public static int $defaultConnectionTimeout = 10;

    /**
     * Default timeout (in seconds) for making requests
     */
    public static int $defaultRequestTimeout = 30;

    /**
     * Resolve the sender with a callback
     *
     * @var callable|null
     */
    private static mixed $senderResolver = null;

    /**
     * Global Middleware Pipeline
     */
    private static ?MiddlewarePipeline $globalMiddlewarePipeline = null;

    /**
     * Write a custom sender resolver
     */
    public static function setSenderResolver(?callable $senderResolver): void
    {
        self::$senderResolver = $senderResolver;
    }

    /**
     * Create a new default sender
     */
    public static function getDefaultSender(): Sender
    {
        $senderResolver = self::$senderResolver;

        return is_callable($senderResolver) ? $senderResolver() : new self::$defaultSender;
    }

    /**
     * Update global middleware
     */
    public static function globalMiddleware(): MiddlewarePipeline
    {
        return self::$globalMiddlewarePipeline ??= new MiddlewarePipeline;
    }

    /**
     * Reset global middleware
     */
    public static function clearGlobalMiddleware(): void
    {
        self::$globalMiddlewarePipeline = null;
    }

    /**
     * Throw an exception if a request without a MockClient is made.
     */
    public static function preventStrayRequests(): void
    {
        self::globalMiddleware()->onRequest(static function (PendingRequest $pendingRequest) {
            if (! $pendingRequest->hasMockClient()) {
                throw new StrayRequestException;
            }
        }, order: PipeOrder::LAST);
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Contracts/IntegerStore.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Contracts;

interface IntegerStore
{
    /**
     * Set a value inside the repository
     *
     * @return $this
     */
    public function set(?int $value): static;

    /**
     * Retrieve all in the repository
     */
    public function get(): ?int;

    /**
     * Determine if the repository is empty
     */
    public function isEmpty(): bool;

    /**
     * Determine if the repository is not empty
     */
    public function isNotEmpty(): bool;
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Contracts/MultipartBodyFactory.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Contracts;

use Saloon\Data\MultipartValue;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\StreamFactoryInterface;

interface MultipartBodyFactory
{
    /**
     * Create a multipart body
     *
     * @param array<MultipartValue> $multipartValues
     */
    public function create(StreamFactoryInterface $streamFactory, array $multipartValues, string $boundary): StreamInterface;
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Contracts/RequestMiddleware.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Contracts;

use Saloon\Http\PendingRequest;

interface RequestMiddleware
{
    /**
     * Register a request middleware
     *
     * @return \Saloon\Http\PendingRequest|FakeResponse|void
     */
    public function __invoke(PendingRequest $pendingRequest);
}

```````


`/home/ddebowczyk/projects/_php/saloon/src/Contracts/ArrayStore.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Contracts;

interface ArrayStore
{
    /**
     * Retrieve all the items.
     *
     * @return array<string, mixed>
     */
    public function all(): array;

    /**
     * Retrieve a single item.
     */
    public function get(string $key, mixed $default = null): mixed;

    /**
     * Overwrite the entire repository's contents.
     *
     * @param array<string, mixed> $data
     * @return $this
     */
    public function set(array $data): static;

    /**
     * Merge in other arrays.
     *
     * @param array<string, mixed> ...$arrays
     * @return $this
     */
    public function merge(array ...$arrays): static;

    /**
     * Add an item to the repository.
     *
     * @return $this
     */
    public function add(string $key, mixed $value): static;

    /**
     * Remove an item from the store.
     *
     * @return $this
     */
    public function remove(string $key): static;

    /**
     * Determine if the store is empty
     */
    public function isEmpty(): bool;

    /**
     * Determine if the store is not empty
     */
    public function isNotEmpty(): bool;
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Contracts/Sender.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Contracts;

use Saloon\Http\Response;
use Saloon\Http\PendingRequest;
use Saloon\Data\FactoryCollection;
use GuzzleHttp\Promise\PromiseInterface;

interface Sender
{
    /**
     * Get the factory collection
     */
    public function getFactoryCollection(): FactoryCollection;

    /**
     * Send the request synchronously
     */
    public function send(PendingRequest $pendingRequest): Response;

    /**
     * Send the request asynchronously
     */
    public function sendAsync(PendingRequest $pendingRequest): PromiseInterface;
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Contracts/DataObjects/WithResponse.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Contracts\DataObjects;

use Saloon\Http\Response;

interface WithResponse
{
    /**
     * Set the response on the data object.
     *
     * @return $this
     */
    public function setResponse(Response $response): static;

    /**
     * Get the response on the data object.
     */
    public function getResponse(): Response;
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Contracts/Authenticator.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Contracts;

use Saloon\Http\PendingRequest;

interface Authenticator
{
    /**
     * Apply the authentication to the request.
     */
    public function set(PendingRequest $pendingRequest): void;
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Contracts/ResponseMiddleware.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Contracts;

use Saloon\Http\Response;

interface ResponseMiddleware
{
    /**
     * Register a response middleware
     *
     * @return \Saloon\Http\Response|void
     */
    public function __invoke(Response $response);
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Contracts/Body/BodyRepository.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Contracts\Body;

use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\StreamFactoryInterface;

interface BodyRepository
{
    /**
     * Set the raw data in the repository
     *
     * @return $this
     */
    public function set(mixed $value): static;

    /**
     * Get the raw data in the repository.
     */
    public function all(): mixed;

    /**
     * Determine if the repository is empty
     */
    public function isEmpty(): bool;

    /**
     * Determine if the repository is not empty
     */
    public function isNotEmpty(): bool;

    /**
     * Convert the body repository into a stream
     */
    public function toStream(StreamFactoryInterface $streamFactory): StreamInterface;
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Contracts/Body/HasBody.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Contracts\Body;

interface HasBody
{
    /**
     * Define Data
     */
    public function body(): BodyRepository;
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Contracts/Body/MergeableBody.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Contracts\Body;

interface MergeableBody
{
    /**
     * Merge another array into the repository
     *
     * @param array<mixed, mixed> ...$arrays
     * @return $this
     */
    public function merge(array ...$arrays): static;
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Enums/PipeOrder.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Enums;

enum PipeOrder: string
{
    case FIRST = 'first';
    case LAST = 'last';
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Enums/Method.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Enums;

enum Method: string
{
    case GET = 'GET';
    case HEAD = 'HEAD';
    case POST = 'POST';
    case PUT = 'PUT';
    case PATCH = 'PATCH';
    case DELETE = 'DELETE';
    case OPTIONS = 'OPTIONS';
    case CONNECT = 'CONNECT';
    case TRACE = 'TRACE';
}

```````


`/home/ddebowczyk/projects/_php/saloon/src/Helpers/Helpers.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Helpers;

use Closure;
use ReflectionClass;
use Saloon\Http\Request;
use Saloon\Http\Connector;
use Saloon\Http\PendingRequest;

/**
 * @internal
 */
final class Helpers
{
    /**
     * Returns all traits used by a class, its parent classes and trait of their traits.
     *
     * @param object|class-string $class
     * @return array<class-string, class-string>
     */
    public static function classUsesRecursive(object|string $class): array
    {
        if (is_object($class)) {
            $class = get_class($class);
        }

        $results = [];

        foreach (array_reverse(class_parents($class)) + [$class => $class] as $class) {
            $results += static::traitUsesRecursive($class);
        }

        return array_unique($results);
    }

    /**
     * Returns all traits used by a trait and its traits.
     *
     * @param class-string $trait
     * @return array<class-string, class-string>
     */
    public static function traitUsesRecursive(string $trait): array
    {
        /** @var array<class-string, class-string> $traits */
        $traits = class_uses($trait) ?: [];

        foreach ($traits as $trait) {
            $traits += static::traitUsesRecursive($trait);
        }

        return $traits;
    }

    /**
     * Return the default value of the given value.
     */
    public static function value(mixed $value, mixed ...$args): mixed
    {
        return $value instanceof Closure ? $value(...$args) : $value;
    }

    /**
     * Check if a class is a subclass of another.
     *
     * @param class-string $class
     */
    public static function isSubclassOf(string $class, string $subclass): bool
    {
        if ($class === $subclass) {
            return true;
        }

        return (new ReflectionClass($class))->isSubclassOf($subclass);
    }

    /**
     * Boot a plugin
     *
     * @param class-string $trait
     * @throws \ReflectionException
     */
    public static function bootPlugin(PendingRequest $pendingRequest, Connector|Request $resource, string $trait): void
    {
        $traitReflection = new ReflectionClass($trait);

        $bootMethodName = 'boot' . $traitReflection->getShortName();

        if (! method_exists($resource, $bootMethodName)) {
            return;
        }

        $resource->{$bootMethodName}($pendingRequest);
    }
}

```````


`/home/ddebowczyk/projects/_php/saloon/src/Helpers/ObjectHelpers.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Helpers;

/**
 * @internal
 */
final class ObjectHelpers
{
    /**
     * Get an item from an object using "dot" notation.
     */
    public static function get(object $object, string $key, mixed $default = null): mixed
    {
        // Split the dot notation into individual keys

        $keys = explode('.', $key);

        // Navigate through the object properties

        foreach ($keys as $dot) {
            // Check if the object is an array or object and if the key exists
            if (is_object($object) && isset($object->{$dot})) {
                $object = $object->{$dot};
            } elseif (is_array($object) && isset($object[$dot])) {
                $object = $object[$dot];
            } else {
                // Return null if the key doesn't exist
                return null;
            }
        }

        return $object;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Helpers/StringHelpers.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Helpers;

/**
 * @internal
 */
final class StringHelpers
{
    /**
     * Determine if a given string matches a given pattern.
     *
     * @param string|iterable<string> $pattern
     */
    public static function matchesPattern(string|iterable $pattern, string $value): bool
    {
        if (! is_iterable($pattern)) {
            $pattern = [$pattern];
        }

        foreach ($pattern as $pattern) {
            $pattern = (string)$pattern;

            // If the given value is an exact match we can of course return true right
            // from the beginning. Otherwise, we will translate asterisks and do an
            // actual pattern match against the two strings to see if they match.
            if ($pattern === $value) {
                return true;
            }

            $pattern = preg_quote($pattern, '#');

            // Asterisks are translated into zero-or-more regular expression wildcards
            // to make it convenient to check if the strings starts with the given
            // pattern such as "library/*", making any string check convenient.
            $pattern = str_replace('\*', '.*', $pattern);

            if (preg_match('#^' . $pattern . '\z#u', $value) === 1) {
                return true;
            }
        }

        return false;
    }

    /**
     * Begin a string with a single instance of a given value.
     */
    public static function start(string $value, string $prefix): string
    {
        $quoted = preg_quote($prefix, '/');

        return $prefix . preg_replace('/^(?:' . $quoted . ')+/u', '', $value);
    }

    /**
     * Generate a more truly "random" alpha-numeric string.
     *
     * @param int<1, max> $length
     * @throws \Exception
     */
    public static function random(int $length = 16): string
    {
        $string = '';

        while (($len = mb_strlen($string)) < $length) {
            /** @var int<1, max> $size */
            $size = $length - $len;

            $bytes = random_bytes($size);

            $string .= mb_substr(str_replace(['/', '+', '='], '', base64_encode($bytes)), 0, $size);
        }

        return $string;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Helpers/URLHelper.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Helpers;

/**
 * @internal
 */
class URLHelper
{
    /**
     * Check if a URL matches a given pattern
     */
    public static function matches(string $pattern, string $value): bool
    {
        return StringHelpers::matchesPattern(StringHelpers::start($pattern, '*'), $value);
    }

    /**
     * Join a base url and an endpoint together.
     */
    public static function join(string $baseUrl, string $endpoint): string
    {
        if (static::isValidUrl($endpoint)) {
            return $endpoint;
        }

        if ($endpoint !== '/') {
            $endpoint = ltrim($endpoint, '/ ');
        }

        $requiresTrailingSlash = ! empty($endpoint) && $endpoint !== '/';

        $baseEndpoint = rtrim($baseUrl, '/ ');

        $baseEndpoint = $requiresTrailingSlash ? $baseEndpoint . '/' : $baseEndpoint;

        return $baseEndpoint . $endpoint;
    }

    /**
     * Check if the URL is a valid URL
     */
    public static function isValidUrl(string $url): bool
    {
        return ! empty(filter_var($url, FILTER_VALIDATE_URL));
    }

    /**
     * Parse a query string into an array
     *
     * @return array<string, mixed>
     */
    public static function parseQueryString(string $query): array
    {
        if ($query === '') {
            return [];
        }

        $parameters = [];

        foreach (explode('&', $query) as $parameter) {
            $name = urldecode((string)strtok($parameter, '='));
            $value = urldecode((string)strtok('='));

            if (! $name || str_starts_with($parameter, '=')) {
                continue;
            }

            $parameters[$name] = $value;
        }

        return $parameters;
    }
}

```````


`/home/ddebowczyk/projects/_php/saloon/src/Helpers/RequestExceptionHelper.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Helpers;

use Throwable;
use Saloon\Http\Response;
use Saloon\Exceptions\Request\ClientException;
use Saloon\Exceptions\Request\ServerException;
use Saloon\Exceptions\Request\RequestException;
use Saloon\Exceptions\Request\Statuses\NotFoundException;
use Saloon\Exceptions\Request\Statuses\ForbiddenException;
use Saloon\Exceptions\Request\Statuses\UnauthorizedException;
use Saloon\Exceptions\Request\Statuses\GatewayTimeoutException;
use Saloon\Exceptions\Request\Statuses\RequestTimeOutException;
use Saloon\Exceptions\Request\Statuses\PaymentRequiredException;
use Saloon\Exceptions\Request\Statuses\TooManyRequestsException;
use Saloon\Exceptions\Request\Statuses\MethodNotAllowedException;
use Saloon\Exceptions\Request\Statuses\ServiceUnavailableException;
use Saloon\Exceptions\Request\Statuses\InternalServerErrorException;
use Saloon\Exceptions\Request\Statuses\UnprocessableEntityException;

class RequestExceptionHelper
{
    /**
     * Create the request exception from a response
     */
    public static function create(Response $response, Throwable $previous = null): RequestException
    {
        $status = $response->status();

        $requestException = match (true) {
            // Built-in exceptions
            $status === 401 => UnauthorizedException::class,
            $status === 402 => PaymentRequiredException::class,
            $status === 403 => ForbiddenException::class,
            $status === 404 => NotFoundException::class,
            $status === 405 => MethodNotAllowedException::class,
            $status === 408 => RequestTimeOutException::class,
            $status === 422 => UnprocessableEntityException::class,
            $status === 429 => TooManyRequestsException::class,
            $status === 500 => InternalServerErrorException::class,
            $status === 503 => ServiceUnavailableException::class,
            $status === 504 => GatewayTimeoutException::class,

            // Fall-back exceptions
            $response->serverError() => ServerException::class,
            $response->clientError() => ClientException::class,
            default => RequestException::class,
        };

        return new $requestException($response, null, 0, $previous);
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Helpers/Storage.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Helpers;

use Saloon\Exceptions\DirectoryNotFoundException;
use Saloon\Exceptions\UnableToCreateFileException;
use Saloon\Exceptions\UnableToCreateDirectoryException;

/**
 * @internal
 */
class Storage
{
    /**
     * The base directory to access the files.
     */
    protected string $baseDirectory;

    /**
     * Constructor
     *
     * @throws \Saloon\Exceptions\DirectoryNotFoundException
     * @throws \Saloon\Exceptions\UnableToCreateDirectoryException
     */
    public function __construct(string $baseDirectory, bool $createMissingBaseDirectory = false)
    {
        if (! is_dir($baseDirectory)) {
            $createMissingBaseDirectory ? $this->createDirectory($baseDirectory) : throw new DirectoryNotFoundException($baseDirectory);
        }

        $this->baseDirectory = $baseDirectory;
    }

    /**
     * Get the base directory
     */
    public function getBaseDirectory(): string
    {
        return $this->baseDirectory;
    }

    /**
     * Combine the base directory with a path.
     */
    protected function buildPath(string $path): string
    {
        $trimRules = DIRECTORY_SEPARATOR . ' ';

        return rtrim($this->baseDirectory, $trimRules) . DIRECTORY_SEPARATOR . ltrim($path, $trimRules);
    }

    /**
     * Check if the file exists
     */
    public function exists(string $path): bool
    {
        return file_exists($this->buildPath($path));
    }

    /**
     * Check if the file is missing
     */
    public function missing(string $path): bool
    {
        return ! $this->exists($path);
    }

    /**
     * Retrieve an item from storage
     */
    public function get(string $path): bool|string
    {
        return file_get_contents($this->buildPath($path));
    }

    /**
     * Put an item in storage
     *
     * @return $this
     * @throws \Saloon\Exceptions\UnableToCreateDirectoryException
     * @throws \Saloon\Exceptions\UnableToCreateFileException
     */
    public function put(string $path, string $contents): static
    {
        $fullPath = $this->buildPath($path);

        $directoryWithoutFilename = implode(DIRECTORY_SEPARATOR, explode(DIRECTORY_SEPARATOR, $fullPath, -1));

        if (empty($directoryWithoutFilename) === false && is_dir($directoryWithoutFilename) === false) {
            $this->createDirectory($directoryWithoutFilename);
        }

        $createdFile = file_put_contents($fullPath, $contents);

        if ($createdFile === false) {
            throw new UnableToCreateFileException($fullPath);
        }

        return $this;
    }

    /**
     * Create a directory
     *
     * @throws \Saloon\Exceptions\UnableToCreateDirectoryException
     */
    public function createDirectory(string $directory): bool
    {
        $createdDirectory = mkdir($directory, 0777, true);

        if ($createdDirectory === false && is_dir($directory) === false) {
            throw new UnableToCreateDirectoryException($directory);
        }

        return true;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Helpers/StatusCodeHelper.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Helpers;

class StatusCodeHelper
{
    /**
     * Get the message for the HTTP code
     */
    public static function getMessage(int $code): ?string
    {
        return match ($code) {
            100 => 'Continue',
            101 => 'Switching Protocols',
            102 => 'Processing',
            200 => 'OK',
            201 => 'Created',
            202 => 'Accepted',
            203 => 'Non-Authoritative Information',
            204 => 'No Content',
            205 => 'Reset Content',
            206 => 'Partial Content',
            207 => 'Multi-status',
            208 => 'Already Reported',
            300 => 'Multiple Choices',
            301 => 'Moved Permanently',
            302 => 'Found',
            303 => 'See Other',
            304 => 'Not Modified',
            305 => 'Use Proxy',
            306 => 'Switch Proxy',
            307 => 'Temporary Redirect',
            308 => 'Permanent Redirect',
            400 => 'Bad Request',
            401 => 'Unauthorized',
            402 => 'Payment Required',
            403 => 'Forbidden',
            404 => 'Not Found',
            405 => 'Method Not Allowed',
            406 => 'Not Acceptable',
            407 => 'Proxy Authentication Required',
            408 => 'Request Time-out',
            409 => 'Conflict',
            410 => 'Gone',
            411 => 'Length Required',
            412 => 'Precondition Failed',
            413 => 'Request Entity Too Large',
            414 => 'Request-URI Too Large',
            415 => 'Unsupported Media Type',
            416 => 'Requested range not satisfiable',
            417 => 'Expectation Failed',
            418 => 'I\'m a teapot',
            422 => 'Unprocessable Entity',
            423 => 'Locked',
            424 => 'Failed Dependency',
            425 => 'Unordered Collection',
            426 => 'Upgrade Required',
            428 => 'Precondition Required',
            429 => 'Too Many Requests',
            431 => 'Request Header Fields Too Large',
            451 => 'Unavailable For Legal Reasons',
            500 => 'Internal Server Error',
            501 => 'Not Implemented',
            502 => 'Bad Gateway',
            503 => 'Service Unavailable',
            504 => 'Gateway Time-out',
            505 => 'HTTP Version not supported',
            506 => 'Variant Also Negotiates',
            507 => 'Insufficient Storage',
            508 => 'Loop Detected',
            510 => 'Not Extended',
            511 => 'Network Authentication Required',
            default => null,
        };
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Helpers/MiddlewarePipeline.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Helpers;

use Saloon\Http\Response;
use Saloon\Enums\PipeOrder;
use Saloon\Http\PendingRequest;
use Saloon\Contracts\FakeResponse;
use Saloon\Exceptions\Request\FatalRequestException;

class MiddlewarePipeline
{
    /**
     * Request Pipeline
     */
    protected Pipeline $requestPipeline;

    /**
     * Response Pipeline
     */
    protected Pipeline $responsePipeline;

    /**
     * Fatal Pipeline
     */
    protected Pipeline $fatalPipeline;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->requestPipeline = new Pipeline;
        $this->responsePipeline = new Pipeline;
        $this->fatalPipeline = new Pipeline;
    }

    /**
     * Add a middleware before the request is sent
     *
     * @param callable(\Saloon\Http\PendingRequest): (\Saloon\Http\PendingRequest|\Saloon\Contracts\FakeResponse|void) $callable
     * @return $this
     */
    public function onRequest(callable $callable, ?string $name = null, ?PipeOrder $order = null): static
    {
        /**
         * For some reason, PHP is not destructing non-static Closures, or 'things' using non-static Closures, correctly, keeping unused objects intact.
         * Using a *static* Closure, or re-binding it to an empty, anonymous class/object is a workaround for the issue.
         * If we don't, things using the MiddlewarePipeline, in turn, won't destruct.
         * Concretely speaking, for Saloon, this means that the Connector will *not* get destructed, and thereby also not the underlying client.
         * Which in turn leaves open file handles until the process terminates.
         *
         * Do note that this is entirely about our *wrapping* Closure below.
         * The provided callable doesn't affect the MiddlewarePipeline.
         */
        $this->requestPipeline->pipe(static function (PendingRequest $pendingRequest) use ($callable): PendingRequest {
            $result = $callable($pendingRequest);

            if ($result instanceof PendingRequest) {
                return $result;
            }

            if ($result instanceof FakeResponse) {
                $pendingRequest->setFakeResponse($result);
            }

            return $pendingRequest;
        }, $name, $order);

        return $this;
    }

    /**
     * Add a middleware after the request is sent
     *
     * @param callable(\Saloon\Http\Response): (\Saloon\Http\Response|void) $callable
     * @return $this
     */
    public function onResponse(callable $callable, ?string $name = null, ?PipeOrder $order = null): static
    {
        /**
         * For some reason, PHP is not destructing non-static Closures, or 'things' using non-static Closures, correctly, keeping unused objects intact.
         * Using a *static* Closure, or re-binding it to an empty, anonymous class/object is a workaround for the issue.
         * If we don't, things using the MiddlewarePipeline, in turn, won't destruct.
         * Concretely speaking, for Saloon, this means that the Connector will *not* get destructed, and thereby also not the underlying client.
         * Which in turn leaves open file handles until the process terminates.
         *
         * Do note that this is entirely about our *wrapping* Closure below.
         * The provided callable doesn't affect the MiddlewarePipeline.
         */
        $this->responsePipeline->pipe(static function (Response $response) use ($callable): Response {
            $result = $callable($response);

            return $result instanceof Response ? $result : $response;
        }, $name, $order);

        return $this;
    }

    /**
     * Add a middleware to run on fatal errors
     *
     * @param callable(FatalRequestException): (void) $callable
     * @return $this
     */
    public function onFatalException(callable $callable, ?string $name = null, ?PipeOrder $order = null): static
    {
        /**
         * For some reason, PHP is not destructing non-static Closures, or 'things' using non-static Closures, correctly, keeping unused objects intact.
         * Using a *static* Closure, or re-binding it to an empty, anonymous class/object is a workaround for the issue.
         * If we don't, things using the MiddlewarePipeline, in turn, won't destruct.
         * Concretely speaking, for Saloon, this means that the Connector will *not* get destructed, and thereby also not the underlying client.
         * Which in turn leaves open file handles until the process terminates.
         *
         * Do note that this is entirely about our *wrapping* Closure below.
         * The provided callable doesn't affect the MiddlewarePipeline.
         */
        $this->fatalPipeline->pipe(static function (FatalRequestException $throwable) use ($callable): FatalRequestException {
            $callable($throwable);

            return $throwable;
        }, $name, $order);

        return $this;
    }

    /**
     * Process the request pipeline.
     */
    public function executeRequestPipeline(PendingRequest $pendingRequest): PendingRequest
    {
        return $this->requestPipeline->process($pendingRequest);
    }

    /**
     * Process the response pipeline.
     */
    public function executeResponsePipeline(Response $response): Response
    {
        return $this->responsePipeline->process($response);
    }

    /**
     * Process the fatal pipeline.
     * @throws \Saloon\Exceptions\Request\FatalRequestException
     */
    public function executeFatalPipeline(FatalRequestException $throwable): void
    {
        $this->fatalPipeline->process($throwable);
    }

    /**
     * Merge in another middleware pipeline.
     *
     * @return $this
     */
    public function merge(MiddlewarePipeline $middlewarePipeline): static
    {
        $requestPipes = array_merge(
            $this->getRequestPipeline()->getPipes(),
            $middlewarePipeline->getRequestPipeline()->getPipes()
        );

        $responsePipes = array_merge(
            $this->getResponsePipeline()->getPipes(),
            $middlewarePipeline->getResponsePipeline()->getPipes()
        );

        $fatalPipes = array_merge(
            $this->getFatalPipeline()->getPipes(),
            $middlewarePipeline->getFatalPipeline()->getPipes()
        );

        $this->requestPipeline->setPipes($requestPipes);
        $this->responsePipeline->setPipes($responsePipes);
        $this->fatalPipeline->setPipes($fatalPipes);

        return $this;
    }

    /**
     * Get the request pipeline
     */
    public function getRequestPipeline(): Pipeline
    {
        return $this->requestPipeline;
    }

    /**
     * Get the response pipeline
     */
    public function getResponsePipeline(): Pipeline
    {
        return $this->responsePipeline;
    }

    /**
     * Get the fatal pipeline
     */
    public function getFatalPipeline(): Pipeline
    {
        return $this->fatalPipeline;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Helpers/Pipeline.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Helpers;

use Saloon\Data\Pipe;
use Saloon\Enums\PipeOrder;
use Saloon\Exceptions\DuplicatePipeNameException;

class Pipeline
{
    /**
     * The pipes in the pipeline.
     *
     * @var array<\Saloon\Data\Pipe>
     */
    protected array $pipes = [];

    /**
     * Add a pipe to the pipeline
     *
     * @param callable(mixed $payload): (mixed) $callable
     * @return $this
     */
    public function pipe(callable $callable, ?string $name = null, ?PipeOrder $order = null): static
    {
        $pipe = new Pipe($callable, $name, $order);

        if (is_string($name) && $this->pipeExists($name)) {
            throw new DuplicatePipeNameException($name);
        }

        $this->pipes[] = $pipe;

        return $this;
    }

    /**
     * Process the pipeline.
     */
    public function process(mixed $payload): mixed
    {
        foreach ($this->sortPipes() as $pipe) {
            $payload = call_user_func($pipe->callable, $payload);
        }

        return $payload;
    }

    /**
     * Sort the pipes based on the "order" classes
     *
     * @return array<\Saloon\Data\Pipe>
     */
    protected function sortPipes(): array
    {
        $firstPipes = [];
        $nullPipes = [];
        $lastPipes = [];

        // We'll simply loop through each pipe and add them to their respective
        // arrays based on the order type. We'll then merge the arrays.

        foreach ($this->pipes as $pipe) {
            match ($pipe->order) {
                PipeOrder::FIRST => $firstPipes[] = $pipe,
                null => $nullPipes[] = $pipe,
                PipeOrder::LAST => $lastPipes[] = $pipe,
            };
        }

        return array_merge($firstPipes, $nullPipes, $lastPipes);
    }

    /**
     * Set the pipes on the pipeline.
     *
     * @param array<\Saloon\Data\Pipe> $pipes
     * @return $this
     */
    public function setPipes(array $pipes): static
    {
        $this->pipes = [];

        // Loop through each of the pipes and manually add each pipe
        // so we can check if the name already exists.

        foreach ($pipes as $pipe) {
            $this->pipe($pipe->callable, $pipe->name, $pipe->order);
        }

        return $this;
    }

    /**
     * Get all the pipes in the pipeline
     *
     * @return array<\Saloon\Data\Pipe>
     */
    public function getPipes(): array
    {
        return $this->pipes;
    }

    /**
     * Check if a given pipe exists for a name
     */
    protected function pipeExists(string $name): bool
    {
        foreach ($this->pipes as $pipe) {
            if ($pipe->name === $name) {
                return true;
            }
        }

        return false;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Helpers/ArrayHelpers.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Helpers;

use ArrayAccess;

use function is_string;

/**
 * @internal
 */
final class ArrayHelpers
{
    /**
     * Determine whether the given value is array accessible.
     *
     * @phpstan-assert-if-true array|ArrayAccess $value
     */
    private static function accessible(mixed $value): bool
    {
        return is_array($value) || $value instanceof ArrayAccess;
    }

    /**
     * Determine if the given key exists in the provided array.
     *
     * @param array<array-key, mixed>|ArrayAccess<array-key, mixed> $array
     * @param array-key|float $key
     */
    private static function exists(array|ArrayAccess $array, string|int|float $key): bool
    {
        if (is_float($key)) {
            $key = (string)$key;
        }

        return $array instanceof ArrayAccess
            ? $array->offsetExists($key)
            : array_key_exists($key, $array);
    }

    /**
     * Get an item from an array using "dot" notation.
     *
     * @param array<array-key, mixed> $array
     * @param array-key|null $key
     * @return ($key is null ? array<array-key, mixed> : mixed)
     */
    public static function get(array $array, string|int|null $key, mixed $default = null): mixed
    {
        if (! static::accessible($array)) {
            return Helpers::value($default);
        }

        if (is_null($key)) {
            return $array;
        }

        if (static::exists($array, $key)) {
            return $array[$key];
        }

        if (! is_string($key) || ! str_contains($key, '.')) {
            return $array[$key] ?? Helpers::value($default);
        }

        foreach (explode('.', $key) as $segment) {
            if (static::accessible($array) && static::exists($array, $segment)) {
                $array = $array[$segment];
            } else {
                return Helpers::value($default);
            }
        }

        return $array;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Helpers/Debugger.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Helpers;

use Closure;
use Saloon\Http\Response;
use Saloon\Http\PendingRequest;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\VarDumper\VarDumper;

class Debugger
{
    /**
     * Application "Die" handler.
     *
     * Only used for Saloon tests
     */
    public static ?Closure $dieHandler = null;

    /**
     * Debug a request with Symfony Var Dumper
     */
    public static function symfonyRequestDebugger(PendingRequest $pendingRequest, RequestInterface $psrRequest): void
    {
        $headers = [];

        foreach ($psrRequest->getHeaders() as $headerName => $value) {
            $headers[$headerName] = implode(';', $value);
        }

        $className = explode('\\', $pendingRequest->getRequest()::class);
        $label = end($className);

        VarDumper::dump([
            'connector' => $pendingRequest->getConnector()::class,
            'request' => $pendingRequest->getRequest()::class,
            'method' => $psrRequest->getMethod(),
            'uri' => (string)$psrRequest->getUri(),
            'headers' => $headers,
            'body' => (string)$psrRequest->getBody(),
        ], 'Saloon Request (' . $label . ') ->');
    }

    /**
     * Debug a response with Symfony Var Dumper
     */
    public static function symfonyResponseDebugger(Response $response, ResponseInterface $psrResponse): void
    {
        $headers = [];

        foreach ($psrResponse->getHeaders() as $headerName => $value) {
            $headers[$headerName] = implode(';', $value);
        }

        $className = explode('\\', $response->getRequest()::class);
        $label = end($className);

        VarDumper::dump([
            'status' => $response->status(),
            'headers' => $headers,
            'body' => $response->body(),
        ], 'Saloon Response (' . $label . ') ->');
    }

    /**
     * Kill the application
     *
     * This is a method as it can be easily mocked during tests
     */
    public static function die(): void
    {
        $handler = self::$dieHandler ?? static fn () => exit(1);

        $handler();
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Bootable.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits;

use Saloon\Http\PendingRequest;

trait Bootable
{
    /**
     * Handle the boot lifecycle hook
     */
    public function boot(PendingRequest $pendingRequest): void
    {
        //
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Conditionable.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits;

use Saloon\Helpers\Helpers;

trait Conditionable
{
    /**
     * Invoke a callable where a given value returns a truthy value.
     *
     * @param \Closure(): (mixed)|mixed $value
     * @param callable($this, mixed): (void) $callback
     * @param callable($this, mixed): (void)|null $default
     * @return $this
     */
    public function when(mixed $value, callable $callback, callable|null $default = null): static
    {
        $value = Helpers::value($value, $this);

        if ($value) {
            $callback($this, $value);

            return $this;
        }

        if ($default) {
            $default($this, $value);
        }

        return $this;
    }

    /**
     * Invoke a callable when a given value returns a falsy value.
     *
     * @param \Closure(): (mixed)|mixed $value
     * @param callable($this, mixed): (void) $callback
     * @param callable($this, mixed): (void)|null $default
     * @return $this
     */
    public function unless(mixed $value, callable $callback, callable|null $default = null): static
    {
        $value = Helpers::value($value, $this);

        if (! $value) {
            $callback($this, $value);

            return $this;
        }

        if ($default) {
            $default($this, $value);
        }

        return $this;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Plugins/AlwaysThrowOnErrors.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Plugins;

use Saloon\Http\Response;
use Saloon\Enums\PipeOrder;
use Saloon\Http\PendingRequest;

trait AlwaysThrowOnErrors
{
    /**
     * Boot AlwaysThrowOnErrors Plugin
     */
    public static function bootAlwaysThrowOnErrors(PendingRequest $pendingRequest): void
    {
        // This middleware will simply use the "throw" method on the response
        // which will check if the connector/request deems the response as a
        // failure - if it does, it will throw a RequestException.

        $pendingRequest->middleware()->onResponse(
            callable: static fn (Response $response) => $response->throw(),
            name: 'alwaysThrowOnErrors',
            order: PipeOrder::LAST
        );
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Plugins/AcceptsJson.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Plugins;

use Saloon\Http\PendingRequest;

trait AcceptsJson
{
    /**
     * Boot AcceptsJson Plugin
     */
    public static function bootAcceptsJson(PendingRequest $pendingRequest): void
    {
        $pendingRequest->headers()->add('Accept', 'application/json');
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Plugins/HasTimeout.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Plugins;

use Saloon\Config;
use GuzzleHttp\RequestOptions;
use Saloon\Http\PendingRequest;

trait HasTimeout
{
    /**
     * Boot HasTimeout plugin.
     */
    public function bootHasTimeout(PendingRequest $pendingRequest): void
    {
        $pendingRequest->config()->merge([
            RequestOptions::CONNECT_TIMEOUT => $this->getConnectTimeout(),
            RequestOptions::TIMEOUT => $this->getRequestTimeout(),
        ]);
    }

    /**
     * Get the request connection timeout.
     */
    public function getConnectTimeout(): float
    {
        return property_exists($this, 'connectTimeout') ? $this->connectTimeout : Config::$defaultConnectionTimeout;
    }

    /**
     * Get the request timeout.
     */
    public function getRequestTimeout(): float
    {
        return property_exists($this, 'requestTimeout') ? $this->requestTimeout : Config::$defaultRequestTimeout;
    }
}

```````


`/home/ddebowczyk/projects/_php/saloon/src/Traits/Auth/AuthenticatesRequests.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Auth;

use Saloon\Contracts\Authenticator;
use Saloon\Http\Auth\BasicAuthenticator;
use Saloon\Http\Auth\QueryAuthenticator;
use Saloon\Http\Auth\TokenAuthenticator;
use Saloon\Http\Auth\DigestAuthenticator;
use Saloon\Http\Auth\HeaderAuthenticator;
use Saloon\Http\Auth\CertificateAuthenticator;

trait AuthenticatesRequests
{
    /**
     * The authenticator used in requests.
     */
    protected ?Authenticator $authenticator = null;

    /**
     * Default authenticator used.
     */
    protected function defaultAuth(): ?Authenticator
    {
        return null;
    }

    /**
     * Retrieve the authenticator.
     */
    public function getAuthenticator(): ?Authenticator
    {
        return $this->authenticator ?? $this->defaultAuth();
    }

    /**
     * Authenticate the request with an authenticator.
     *
     * @return $this
     */
    public function authenticate(Authenticator $authenticator): static
    {
        $this->authenticator = $authenticator;

        return $this;
    }

    /**
     * Authenticate the request with an Authorization header.
     *
     * @deprecated This method will be removed in Saloon v4. You should use the defaultAuth method or the `->authenticate(new TokenAuthenticator)` instead.
     * @return $this
     */
    public function withTokenAuth(string $token, string $prefix = 'Bearer'): static
    {
        return $this->authenticate(new TokenAuthenticator($token, $prefix));
    }

    /**
     * Authenticate the request with "basic" authentication.
     *
     * @deprecated This method will be removed in Saloon v4. You should use the defaultAuth method or the `->authenticate(new BasicAuthenticator)` instead.
     * @return $this
     */
    public function withBasicAuth(string $username, string $password): static
    {
        return $this->authenticate(new BasicAuthenticator($username, $password));
    }

    /**
     * Authenticate the request with "digest" authentication.
     *
     * @deprecated This method will be removed in Saloon v4. You should use the defaultAuth method or the `->authenticate(new DigestAuthenticator)` instead.
     * @return $this
     */
    public function withDigestAuth(string $username, string $password, string $digest): static
    {
        return $this->authenticate(new DigestAuthenticator($username, $password, $digest));
    }

    /**
     * Authenticate the request with a query parameter token.
     *
     * @deprecated This method will be removed in Saloon v4. You should use the defaultAuth method or the `->authenticate(new QueryAuthenticator)` instead.
     * @return $this
     */
    public function withQueryAuth(string $parameter, string $value): static
    {
        return $this->authenticate(new QueryAuthenticator($parameter, $value));
    }

    /**
     * Authenticate the request with a header.
     *
     * @deprecated This method will be removed in Saloon v4. You should use the defaultAuth method or the `->authenticate(new HeaderAuthenticator)` instead.
     * @return $this
     */
    public function withHeaderAuth(string $accessToken, string $headerName = 'Authorization'): static
    {
        return $this->authenticate(new HeaderAuthenticator($accessToken, $headerName));
    }

    /**
     * Authenticate the request with a certificate.
     *
     * @deprecated This method will be removed in Saloon v4. You should use the defaultAuth method or the `->authenticate(new CertificateAuthenticator)` instead.
     * @return $this
     */
    public function withCertificateAuth(string $path, ?string $password = null): static
    {
        return $this->authenticate(new CertificateAuthenticator($path, $password));
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Auth/RequiresAuth.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Auth;

use Saloon\Http\PendingRequest;
use Saloon\Contracts\Authenticator;
use Saloon\Exceptions\MissingAuthenticatorException;

trait RequiresAuth
{
    /**
     * Throw an exception if an authenticator is not on the request while it is booting.
     *
     * @throws \Saloon\Exceptions\MissingAuthenticatorException
     */
    public function bootRequiresAuth(PendingRequest $pendingSaloonRequest): void
    {
        $authenticator = $pendingSaloonRequest->getAuthenticator();

        if (! $authenticator instanceof Authenticator) {
            throw new MissingAuthenticatorException($this->getRequiresAuthMessage($pendingSaloonRequest));
        }
    }

    /**
     * Default message.
     */
    protected function getRequiresAuthMessage(PendingRequest $pendingRequest): string
    {
        return sprintf('The "%s" request requires authentication.', $pendingRequest->getRequest()::class);
    }
}

```````


`/home/ddebowczyk/projects/_php/saloon/src/Traits/PendingRequest/ManagesPsrRequests.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\PendingRequest;

use Saloon\Helpers\URLHelper;
use Psr\Http\Message\UriInterface;
use Saloon\Data\FactoryCollection;
use Psr\Http\Message\RequestInterface;
use Saloon\Contracts\Body\BodyRepository;

trait ManagesPsrRequests
{
    /**
     * The factory collection.
     */
    protected FactoryCollection $factoryCollection;

    /**
     * Get the URI for the pending request.
     */
    public function getUri(): UriInterface
    {
        $uri = $this->factoryCollection->uriFactory->createUri($this->getUrl());

        // We'll parse the existing query parameters from the URL (if they have been defined)
        // and then we'll merge in Saloon's query parameters. Our query parameters will take
        // priority over any that were defined in the URL.

        $existingQuery = URLHelper::parseQueryString($uri->getQuery());

        return $uri->withQuery(
            http_build_query(array_merge($existingQuery, $this->query()->all()))
        );
    }

    /**
     * Get the PSR-7 request
     */
    public function createPsrRequest(): RequestInterface
    {
        $factories = $this->factoryCollection;

        $request = $factories->requestFactory->createRequest(
            method: $this->getMethod()->value,
            uri: $this->getUri(),
        );

        foreach ($this->headers()->all() as $headerName => $headerValue) {
            $request = $request->withHeader($headerName, $headerValue);
        }

        if ($this->body() instanceof BodyRepository) {
            $request = $request->withBody($this->body()->toStream($factories->streamFactory));
        }

        // Now we'll run our event hooks on both the connector and request which allows the
        // user to be able to make any final changes to the PSR request if they need to
        // like modifying the URI or adding extra headers.

        $request = $this->connector->handlePsrRequest($request, $this);

        return $this->request->handlePsrRequest($request, $this);
    }

    /**
     * Get the factory collection
     */
    public function getFactoryCollection(): FactoryCollection
    {
        return $this->factoryCollection;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Macroable.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits;

use Closure;
use ReflectionClass;
use ReflectionMethod;
use BadMethodCallException;

/**
 * Many thanks to Spatie for building this excellent trait.
 *
 * @see https://github.com/spatie/macroable
 */
trait Macroable
{
    /**
     * Macros stored
     *
     * @var array<object|callable>
     */
    protected static array $macros = [];

    /**
     * Create a macro
     */
    public static function macro(string $name, object|callable $macro): void
    {
        static::$macros[$name] = $macro;
    }

    /**
     * Add a mixin
     *
     * @param object|class-string $mixin
     */
    public static function mixin(object|string $mixin): void
    {
        $methods = (new ReflectionClass($mixin))->getMethods(
            ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
        );

        foreach ($methods as $method) {
            $method->setAccessible(true);

            static::macro($method->name, $method->invoke($mixin));
        }
    }

    /**
     * Check if we have a macro
     */
    public static function hasMacro(string $name): bool
    {
        return isset(static::$macros[$name]);
    }

    /**
     * Handle a static call
     *
     * @param array<string, mixed> $parameters
     */
    public static function __callStatic(string $method, array $parameters): mixed
    {
        if (! static::hasMacro($method)) {
            throw new BadMethodCallException("Method {$method} does not exist.");
        }

        $macro = static::$macros[$method];

        if ($macro instanceof Closure) {
            return call_user_func_array(Closure::bind($macro, null, static::class), $parameters);
        }

        return call_user_func_array($macro, $parameters);
    }

    /**
     * Handle a method call
     *
     * @param array<string, mixed> $parameters
     */
    public function __call(string $method, array $parameters): mixed
    {
        if (! static::hasMacro($method)) {
            throw new BadMethodCallException("Method {$method} does not exist.");
        }

        $macro = static::$macros[$method];

        if ($macro instanceof Closure) {
            return call_user_func_array($macro->bindTo($this, static::class), $parameters);
        }

        return call_user_func_array($macro, $parameters);
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Makeable.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits;

trait Makeable
{
    /**
     * Instantiate a new class with the arguments.
     */
    public static function make(mixed ...$arguments): static
    {
        return new static(...$arguments);
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/HasDebugging.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits;

use Saloon\Http\Response;
use Saloon\Enums\PipeOrder;
use Saloon\Helpers\Debugger;
use Saloon\Http\PendingRequest;

trait HasDebugging
{
    /**
     * Register a request debugger
     *
     * Leave blank for a default debugger (requires symfony/var-dump)
     *
     * @param callable(\Saloon\Http\PendingRequest, \Psr\Http\Message\RequestInterface): void|null $onRequest
     * @return $this
     */
    public function debugRequest(?callable $onRequest = null, bool $die = false): static
    {
        // When the user has not specified a callable to debug with, we will use this default
        // debugging driver. This will use symfony/var-dumper to display a nice output to
        // the user's screen of the request.

        $onRequest ??= Debugger::symfonyRequestDebugger(...);

        // Register the middleware - we will use PipeOrder::FIRST to ensure that the response
        // is shown before it is modified by the user's middleware.

        $this->middleware()->onRequest(
            callable: static function (PendingRequest $pendingRequest) use ($onRequest, $die): void {
                $onRequest($pendingRequest, $pendingRequest->createPsrRequest());

                if ($die) {
                    Debugger::die();
                }
            },
            order: PipeOrder::LAST
        );

        return $this;
    }

    /**
     * Register a response debugger
     *
     * Leave blank for a default debugger (requires symfony/var-dump)
     *
     * @param callable(\Saloon\Http\Response, \Psr\Http\Message\ResponseInterface): void|null $onResponse
     * @return $this
     */
    public function debugResponse(?callable $onResponse = null, bool $die = false): static
    {
        // When the user has not specified a callable to debug with, we will use this default
        // debugging driver. This will use symfony/var-dumper to display a nice output to
        // the user's screen of the response.

        $onResponse ??= Debugger::symfonyResponseDebugger(...);

        // Register the middleware - we will use PipeOrder::FIRST to ensure that the response
        // is shown before it is modified by the user's middleware.

        $this->middleware()->onResponse(
            callable: static function (Response $response) use ($onResponse, $die): void {
                $onResponse($response, $response->getPsrResponse());

                if ($die) {
                    Debugger::die();
                }
            },
            order: PipeOrder::FIRST
        );

        return $this;
    }

    /**
     * Dump a pretty output of the request and response.
     *
     * This is useful if you would like to see the request right before it is sent
     * to inspect the body and URI to ensure it is correct. You can also inspect
     * the raw response as it comes back.
     *
     * Note that any changes made to the PSR request by the sender will not be
     * reflected by this output.
     *
     * Requires symfony/var-dumper
     */
    public function debug(bool $die = false): static
    {
        return $this->debugRequest()->debugResponse(die: $die);
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/HandlesPsrRequest.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits;

use Saloon\Http\PendingRequest;
use Psr\Http\Message\RequestInterface;

trait HandlesPsrRequest
{
    /**
     * Handle the PSR request before it is sent
     */
    public function handlePsrRequest(RequestInterface $request, PendingRequest $pendingRequest): RequestInterface
    {
        return $request;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/ManagesExceptions.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits;

use Throwable;
use Saloon\Http\Response;

trait ManagesExceptions
{
    /**
     * Determine if the request has failed.
     */
    public function hasRequestFailed(Response $response): ?bool
    {
        return null;
    }

    /**
     * Get the request exception.
     */
    public function getRequestException(Response $response, ?Throwable $senderException): ?Throwable
    {
        return null;
    }

    /**
     * Determine if we should throw an exception if the `$response->throw()` is
     * used, or when the `AlwaysThrowOnErrors` trait is used.
     */
    public function shouldThrowRequestException(Response $response): bool
    {
        return $response->failed();
    }
}

```````


`/home/ddebowczyk/projects/_php/saloon/src/Traits/Connector/SendsRequests.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Connector;

use LogicException;
use Saloon\Http\Pool;
use Saloon\Http\Request;
use Saloon\Http\Response;
use GuzzleHttp\Promise\Utils;
use GuzzleHttp\Promise\Promise;
use Saloon\Http\PendingRequest;
use Saloon\Http\Faking\MockClient;
use GuzzleHttp\Promise\PromiseInterface;
use Saloon\Exceptions\Request\RequestException;
use Saloon\Exceptions\Request\FatalRequestException;

trait SendsRequests
{
    use HasSender;
    use ManagesFakeResponses;

    /**
     * Send a request synchronously
     *
     * @param callable(\Throwable, \Saloon\Http\Request): (bool)|null $handleRetry
     */
    public function send(Request $request, MockClient $mockClient = null, callable $handleRetry = null): Response
    {
        if (is_null($handleRetry)) {
            $handleRetry = static fn (): bool => true;
        }

        $attempts = 0;

        $maxTries = $request->tries ?? $this->tries ?? 1;
        $retryInterval = $request->retryInterval ?? $this->retryInterval ?? 0;
        $throwOnMaxTries = $request->throwOnMaxTries ?? $this->throwOnMaxTries ?? true;
        $useExponentialBackoff = $request->useExponentialBackoff ?? $this->useExponentialBackoff ?? false;

        if ($maxTries <= 0) {
            $maxTries = 1;
        }

        if ($retryInterval <= 0) {
            $retryInterval = 0;
        }

        while ($attempts < $maxTries) {
            $attempts++;

            // When the current attempt is greater than one, we will wait
            // the interval (if it has been provided)

            if ($attempts > 1) {
                $sleepTime = $useExponentialBackoff
                    ? $retryInterval * (2 ** ($attempts - 2)) * 1000
                    : $retryInterval * 1000;

                usleep($sleepTime);
            }

            try {
                $pendingRequest = $this->createPendingRequest($request, $mockClient);

                // 🚀 ... 🪐  ... 💫

                if ($pendingRequest->hasFakeResponse()) {
                    $response = $this->createFakeResponse($pendingRequest);
                } else {
                    $response = $this->sender()->send($pendingRequest);
                }

                // We'll execute the response pipeline now so that all the response
                // middleware can be run before we throw any exceptions.

                $response = $pendingRequest->executeResponsePipeline($response);

                // We'll check if our tries is greater than one. If it is, then we will
                // force an exception to be thrown if the request was unsuccessful.
                // This will then force our catch handler to retry the request.

                if ($maxTries > 1) {
                    $response->throw();
                }

                return $response;
            } catch (FatalRequestException|RequestException $exception) {
                // We'll attempt to get the response from the exception. We'll only be able
                // to do this if the exception was a "RequestException".

                $exceptionResponse = $exception instanceof RequestException ? $exception->getResponse() : null;

                // If the exception is a FatalRequestException, we'll execute the fatal pipeline
                if($exception instanceof FatalRequestException) {
                    $exception->getPendingRequest()->executeFatalPipeline($exception);
                }

                // If we've reached our max attempts - we won't try again, but we'll either
                // return the last response made or just throw an exception.

                if ($attempts === $maxTries) {
                    return isset($exceptionResponse) && $throwOnMaxTries === false ? $exceptionResponse : throw $exception;
                }

                // Now we'll run the "handleRetry" method on both the connector and the request.
                // This method will return a boolean. If just one of the objects returns false
                // then we won't handle the retry.

                $allowRetry = $handleRetry($exception, $request)
                    && $request->handleRetry($exception, $request)
                    && $this->handleRetry($exception, $request);

                // If we cannot retry we will simply return the response or throw the exception.

                if ($allowRetry === false) {
                    return isset($exceptionResponse) && $throwOnMaxTries === false ? $exceptionResponse : throw $exception;
                }
            }
        }

        throw new LogicException('The request was not sent.');
    }

    /**
     * Send a request asynchronously
     */
    public function sendAsync(Request $request, MockClient $mockClient = null): PromiseInterface
    {
        $sender = $this->sender();

        // We'll wrap the following logic in our own Promise which means we won't
        // build up our PendingRequest until the promise is actually being sent
        // this is great because our middleware will only run right before
        // the request is sent.

        return Utils::task(function () use ($request, $mockClient, $sender) {
            $pendingRequest = $this->createPendingRequest($request, $mockClient)->setAsynchronous(true);

            // We need to check if the Pending Request contains a fake response.
            // If it does, then we will create the fake response. Otherwise,
            // we'll send the request.

            // 🚀 ... 🪐  ... 💫

            if ($pendingRequest->hasFakeResponse()) {
                $requestPromise = $this->createFakeResponse($pendingRequest);
            } else {
                $requestPromise = $sender->sendAsync($pendingRequest);
            }

            $requestPromise->then(fn (Response $response) => $pendingRequest->executeResponsePipeline($response));

            return $requestPromise;
        });
    }

    /**
     * Send a synchronous request and retry if it fails
     *
     * @deprecated This method will be removed in Saloon v4. Please refer to the documentation to see connector or request-based retry functionality.
     *
     * @param callable(\Throwable, \Saloon\Http\Request): (bool)|null $handleRetry
     */
    public function sendAndRetry(Request $request, int $tries, int $interval = 0, callable $handleRetry = null, bool $throw = true, MockClient $mockClient = null, bool $useExponentialBackoff = false): Response
    {
        $request->tries = $tries;
        $request->retryInterval = $interval;
        $request->throwOnMaxTries = $throw;
        $request->useExponentialBackoff = $useExponentialBackoff;

        return $this->send($request, $mockClient, $handleRetry);
    }

    /**
     * Create a new PendingRequest
     */
    public function createPendingRequest(Request $request, MockClient $mockClient = null): PendingRequest
    {
        return new PendingRequest($this, $request, $mockClient);
    }

    /**
     * Create a request pool
     *
     * @param iterable<\GuzzleHttp\Promise\PromiseInterface|\Saloon\Http\Request>|callable(\Saloon\Http\Connector): iterable<\GuzzleHttp\Promise\PromiseInterface|\Saloon\Http\Request> $requests
     * @param int|callable(int $pendingRequests): (int) $concurrency
     * @param callable(\Saloon\Http\Response, array-key $key, \GuzzleHttp\Promise\PromiseInterface $poolAggregate): (void)|null $responseHandler
     * @param callable(mixed $reason, array-key $key, \GuzzleHttp\Promise\PromiseInterface $poolAggregate): (void)|null $exceptionHandler
     */
    public function pool(iterable|callable $requests = [], int|callable $concurrency = 5, callable|null $responseHandler = null, callable|null $exceptionHandler = null): Pool
    {
        return new Pool($this, $requests, $concurrency, $responseHandler, $exceptionHandler);
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Connector/HasSender.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Connector;

use Saloon\Config;
use Saloon\Contracts\Sender;

trait HasSender
{
    /**
     * Specify the default sender
     */
    protected string $defaultSender = '';

    /**
     * The request sender.
     */
    protected Sender $sender;

    /**
     * Manage the request sender.
     */
    public function sender(): Sender
    {
        return $this->sender ??= $this->defaultSender();
    }

    /**
     * Define the default request sender.
     */
    protected function defaultSender(): Sender
    {
        if (empty($this->defaultSender)) {
            return Config::getDefaultSender();
        }

        return new $this->defaultSender;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Body/CreatesStreamFromString.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Body;

use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\StreamFactoryInterface;

trait CreatesStreamFromString
{
    /**
     * Convert the body repository into a stream
     */
    public function toStream(StreamFactoryInterface $streamFactory): StreamInterface
    {
        return $streamFactory->createStream((string)$this);
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Body/HasXmlBody.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Body;

use Saloon\Http\PendingRequest;

trait HasXmlBody
{
    use HasStringBody;

    /**
     * Boot the plugin
     */
    public function bootHasXmlBody(PendingRequest $pendingRequest): void
    {
        $pendingRequest->headers()->add('Content-Type', 'application/xml');
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Body/HasStringBody.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Body;

use Saloon\Repositories\Body\StringBodyRepository;

trait HasStringBody
{
    use ChecksForHasBody;

    /**
     * Body Repository
     */
    protected StringBodyRepository $body;

    /**
     * Retrieve the data repository
     */
    public function body(): StringBodyRepository
    {
        return $this->body ??= new StringBodyRepository($this->defaultBody());
    }

    /**
     * Default body
     */
    protected function defaultBody(): ?string
    {
        return null;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Body/HasFormBody.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Body;

use Saloon\Http\PendingRequest;
use Saloon\Repositories\Body\FormBodyRepository;

trait HasFormBody
{
    use ChecksForHasBody;

    /**
     * Body Repository
     */
    protected FormBodyRepository $body;

    /**
     * Boot the HasFormBody trait
     */
    public function bootHasFormBody(PendingRequest $pendingRequest): void
    {
        $pendingRequest->headers()->add('Content-Type', 'application/x-www-form-urlencoded');
    }

    /**
     * Retrieve the data repository
     */
    public function body(): FormBodyRepository
    {
        return $this->body ??= new FormBodyRepository($this->defaultBody());
    }

    /**
     * Default body
     *
     * @return array<string, mixed>
     */
    protected function defaultBody(): array
    {
        return [];
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Body/ChecksForHasBody.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Body;

use Saloon\Http\PendingRequest;
use Saloon\Contracts\Body\HasBody;
use Saloon\Exceptions\BodyException;

trait ChecksForHasBody
{
    /**
     * Check if the request or connector has the WithBody class.
     *
     * @throws \Saloon\Exceptions\BodyException
     */
    public function bootChecksForHasBody(PendingRequest $pendingRequest): void
    {
        if ($pendingRequest->getRequest() instanceof HasBody || $pendingRequest->getConnector() instanceof HasBody) {
            return;
        }

        throw new BodyException(sprintf('You have added a body trait without implementing `%s` on your request or connector.', HasBody::class));
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Body/HasJsonBody.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Body;

use Saloon\Http\PendingRequest;
use Saloon\Repositories\Body\JsonBodyRepository;

trait HasJsonBody
{
    use ChecksForHasBody;

    /**
     * Body Repository
     */
    protected JsonBodyRepository $body;

    /**
     * Boot the plugin
     */
    public function bootHasJsonBody(PendingRequest $pendingRequest): void
    {
        $pendingRequest->headers()->add('Content-Type', 'application/json');
    }

    /**
     * Retrieve the data repository
     */
    public function body(): JsonBodyRepository
    {
        return $this->body ??= new JsonBodyRepository($this->defaultBody());
    }

    /**
     * Default body
     *
     * @return array<string, mixed>
     */
    protected function defaultBody(): array
    {
        return [];
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Body/HasMultipartBody.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Body;

use Saloon\Http\PendingRequest;
use Saloon\Repositories\Body\MultipartBodyRepository;

trait HasMultipartBody
{
    use ChecksForHasBody;

    /**
     * Body Repository
     */
    protected MultipartBodyRepository $body;

    /**
     * Boot the HasMultipartBody trait
     */
    public function bootHasMultipartBody(PendingRequest $pendingRequest): void
    {
        $pendingRequest->headers()->add('Content-Type', 'multipart/form-data; boundary=' . $this->body()->getBoundary());
    }

    /**
     * Retrieve the data repository
     */
    public function body(): MultipartBodyRepository
    {
        return $this->body ??= new MultipartBodyRepository($this->defaultBody());
    }

    /**
     * Default body
     *
     * @return array<\Saloon\Data\MultipartValue>
     */
    protected function defaultBody(): array
    {
        return [];
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Body/HasStreamBody.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Body;

use Psr\Http\Message\StreamInterface;
use Saloon\Repositories\Body\StreamBodyRepository;

trait HasStreamBody
{
    use ChecksForHasBody;

    /**
     * Body Repository
     */
    protected StreamBodyRepository $body;

    /**
     * Retrieve the data repository
     */
    public function body(): StreamBodyRepository
    {
        return $this->body ??= new StreamBodyRepository($this->defaultBody());
    }

    /**
     * Default body
     *
     * @return StreamInterface|resource|null
     */
    protected function defaultBody(): mixed
    {
        return null;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/RequestProperties/HasConfig.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\RequestProperties;

use Saloon\Repositories\ArrayStore;
use Saloon\Contracts\ArrayStore as ArrayStoreContract;

trait HasConfig
{
    /**
     * Request Config
     */
    protected ArrayStoreContract $config;

    /**
     * Access the config
     */
    public function config(): ArrayStoreContract
    {
        return $this->config ??= new ArrayStore($this->defaultConfig());
    }

    /**
     * Default Config
     *
     * @return array<string, mixed>
     */
    protected function defaultConfig(): array
    {
        return [];
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/RequestProperties/HasRequestProperties.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\RequestProperties;

trait HasRequestProperties
{
    use HasHeaders;
    use HasQuery;
    use HasConfig;
    use HasMiddleware;
    use HasDelay;
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/RequestProperties/HasMiddleware.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\RequestProperties;

use Saloon\Helpers\MiddlewarePipeline;

trait HasMiddleware
{
    /**
     * Middleware Pipeline
     */
    protected MiddlewarePipeline $middlewarePipeline;

    /**
     * Access the middleware pipeline
     */
    public function middleware(): MiddlewarePipeline
    {
        return $this->middlewarePipeline ??= new MiddlewarePipeline;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/RequestProperties/HasTries.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\RequestProperties;

use Saloon\Http\Request;
use Saloon\Exceptions\Request\RequestException;
use Saloon\Exceptions\Request\FatalRequestException;

trait HasTries
{
    /**
     * The number of times a request should be retried if a failure response is returned.
     *
     * Set to null to disable the retry functionality.
     */
    public ?int $tries = null;

    /**
     * The interval in milliseconds Saloon should wait between retries.
     *
     * For example 500ms = 0.5 seconds.
     *
     * Set to null to disable the retry interval.
     */
    public ?int $retryInterval = null;

    /**
     * Should Saloon use exponential backoff during retries?
     *
     * When true, Saloon will double the retry interval after each attempt.
     */
    public ?bool $useExponentialBackoff = null;

    /**
     * Should Saloon throw an exception after exhausting the maximum number of retries?
     *
     * When false, Saloon will return the last response attempted.
     *
     * Set to null to always throw after maximum retry attempts.
     */
    public ?bool $throwOnMaxTries = null;

    /**
     * Define whether the request should be retried.
     *
     * You can access the response from the RequestException. You can also modify the
     * request before the next attempt is made.
     */
    public function handleRetry(FatalRequestException|RequestException $exception, Request $request): bool
    {
        return true;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/RequestProperties/HasDelay.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\RequestProperties;

use Saloon\Repositories\IntegerStore;

trait HasDelay
{
    /**
     * Request Delay
     */
    protected IntegerStore $delay;

    /**
     * Delay repository
     */
    public function delay(): IntegerStore
    {
        return $this->delay ??= new IntegerStore($this->defaultDelay());
    }

    /**
     * Default Delay
     */
    protected function defaultDelay(): ?int
    {
        return null;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/RequestProperties/HasHeaders.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\RequestProperties;

use Saloon\Repositories\ArrayStore;
use Saloon\Contracts\ArrayStore as ArrayStoreContract;

trait HasHeaders
{
    /**
     * Request Headers
     */
    protected ArrayStoreContract $headers;

    /**
     * Access the headers
     */
    public function headers(): ArrayStoreContract
    {
        return $this->headers ??= new ArrayStore($this->defaultHeaders());
    }

    /**
     * Default Request Headers
     *
     * @return array<string, mixed>
     */
    protected function defaultHeaders(): array
    {
        return [];
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/RequestProperties/HasQuery.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\RequestProperties;

use Saloon\Repositories\ArrayStore;
use Saloon\Contracts\ArrayStore as ArrayStoreContract;

trait HasQuery
{
    /**
     * Request Query Parameters
     */
    protected ArrayStoreContract $query;

    /**
     * Access the query parameters
     */
    public function query(): ArrayStoreContract
    {
        return $this->query ??= new ArrayStore($this->defaultQuery());
    }

    /**
     * Default Query Parameters
     *
     * @return array<string, mixed>
     */
    protected function defaultQuery(): array
    {
        return [];
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Responses/HasCustomResponses.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Responses;

trait HasCustomResponses
{
    /**
     * Specify a default response.
     *
     * When null or an empty string, the response on the sender will be used.
     *
     * @var class-string<\Saloon\Http\Response>|null
     */
    protected ?string $response = null;

    /**
     * Resolve the custom response class
     *
     * @return class-string<\Saloon\Http\Response>|null
     */
    public function resolveResponseClass(): ?string
    {
        return $this->response ?? null;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Responses/HasResponse.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Responses;

use Saloon\Http\Response;

trait HasResponse
{
    /**
     * The original response.
     */
    protected Response $response;

    /**
     * Set the response on the data object.
     *
     * @return $this
     */
    public function setResponse(Response $response): static
    {
        $this->response = $response;

        return $this;
    }

    /**
     * Get the response on the data object.
     */
    public function getResponse(): Response
    {
        return $this->response;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Request/CreatesDtoFromResponse.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Request;

use Saloon\Http\Response;

trait CreatesDtoFromResponse
{
    /**
     * Cast the response to a DTO.
     */
    public function createDtoFromResponse(Response $response): mixed
    {
        return null;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Traits/Request/HasConnector.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Traits\Request;

use Saloon\Http\Response;
use Saloon\Http\Connector;
use Saloon\Contracts\Sender;
use Saloon\Http\PendingRequest;
use Saloon\Http\Faking\MockClient;
use GuzzleHttp\Promise\PromiseInterface;

trait HasConnector
{
    /**
     * The loaded connector used in requests.
     */
    private ?Connector $loadedConnector = null;

    /**
     *  Retrieve the loaded connector.
     */
    public function connector(): Connector
    {
        return $this->loadedConnector ??= $this->resolveConnector();
    }

    /**
     * Set the loaded connector at runtime.
     *
     * @return $this
     */
    public function setConnector(Connector $connector): static
    {
        $this->loadedConnector = $connector;

        return $this;
    }

    /**
     * Create a new connector instance.
     */
    protected function resolveConnector(): Connector
    {
        return new $this->connector;
    }

    /**
     * Access the HTTP sender
     */
    public function sender(): Sender
    {
        return $this->connector()->sender();
    }

    /**
     * Create a pending request
     */
    public function createPendingRequest(MockClient $mockClient = null): PendingRequest
    {
        return $this->connector()->createPendingRequest($this, $mockClient);
    }

    /**
     * Send a request synchronously
     */
    public function send(MockClient $mockClient = null): Response
    {
        return $this->connector()->send($this, $mockClient);
    }

    /**
     * Send a request asynchronously
     */
    public function sendAsync(MockClient $mockClient = null): PromiseInterface
    {
        return $this->connector()->sendAsync($this, $mockClient);
    }
}

```````


`/home/ddebowczyk/projects/_php/saloon/src/Http/Middleware/ValidateProperties.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http\Middleware;

use Saloon\Http\PendingRequest;
use Saloon\Contracts\RequestMiddleware;
use Saloon\Exceptions\InvalidHeaderException;

class ValidateProperties implements RequestMiddleware
{
    /**
     * Validate the properties on the request before it is sent
     *
     * @throws \Saloon\Exceptions\InvalidHeaderException
     */
    public function __invoke(PendingRequest $pendingRequest): void
    {
        // Validate that each header provided has a string key

        foreach ($pendingRequest->headers()->all() as $key => $unused) {
            if (! is_string($key)) {
                throw new InvalidHeaderException('One or more of the headers are invalid. Make sure to use the header name as the key. For example: [\'Content-Type\' => \'application/json\'].');
            }
        }
    }
}

```````


`/home/ddebowczyk/projects/_php/saloon/src/Http/Middleware/DelayMiddleware.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http\Middleware;

use Saloon\Http\PendingRequest;
use Saloon\Contracts\RequestMiddleware;

class DelayMiddleware implements RequestMiddleware
{
    /**
     * Register a request middleware
     */
    public function __invoke(PendingRequest $pendingRequest): void
    {
        $delay = $pendingRequest->delay()->get() ?? 0;

        usleep($delay * 1000);
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Http/Connector.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http;

use Saloon\Traits\Bootable;
use Saloon\Traits\Makeable;
use Saloon\Traits\HasDebugging;
use Saloon\Traits\Conditionable;
use Saloon\Traits\HasMockClient;
use Saloon\Traits\HandlesPsrRequest;
use Saloon\Traits\ManagesExceptions;
use Saloon\Traits\Connector\SendsRequests;
use Saloon\Traits\Auth\AuthenticatesRequests;
use Saloon\Traits\RequestProperties\HasTries;
use Saloon\Traits\Responses\HasCustomResponses;
use Saloon\Traits\Request\CreatesDtoFromResponse;
use Saloon\Traits\RequestProperties\HasRequestProperties;

abstract class Connector
{
    use CreatesDtoFromResponse;
    use AuthenticatesRequests;
    use HasRequestProperties;
    use HasCustomResponses;
    use ManagesExceptions;
    use HandlesPsrRequest;
    use HasMockClient;
    use SendsRequests;
    use Conditionable;
    use Bootable;
    use Makeable;
    use HasTries;
    use HasDebugging;

    /**
     * Define the base URL of the API.
     */
    abstract public function resolveBaseUrl(): string;
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Http/PendingRequest.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http;

use Saloon\Config;
use Saloon\Enums\Method;
use Saloon\Helpers\Helpers;
use Saloon\Traits\Macroable;
use Saloon\Helpers\URLHelper;
use Saloon\Traits\Conditionable;
use Saloon\Traits\HasMockClient;
use Saloon\Contracts\FakeResponse;
use Saloon\Http\Faking\MockClient;
use Saloon\Contracts\Authenticator;
use Saloon\Contracts\Body\BodyRepository;
use Saloon\Http\PendingRequest\MergeBody;
use Saloon\Http\PendingRequest\MergeDelay;
use Saloon\Http\Middleware\DelayMiddleware;
use Saloon\Http\PendingRequest\BootPlugins;
use Saloon\Traits\Auth\AuthenticatesRequests;
use Saloon\Http\Middleware\ValidateProperties;
use Saloon\Http\Middleware\DetermineMockResponse;
use Saloon\Exceptions\InvalidResponseClassException;
use Saloon\Exceptions\Request\FatalRequestException;
use Saloon\Traits\PendingRequest\ManagesPsrRequests;
use Saloon\Http\PendingRequest\MergeRequestProperties;
use Saloon\Http\PendingRequest\BootConnectorAndRequest;
use Saloon\Traits\RequestProperties\HasRequestProperties;
use Saloon\Http\PendingRequest\AuthenticatePendingRequest;

class PendingRequest
{
    use AuthenticatesRequests;
    use HasRequestProperties;
    use ManagesPsrRequests;
    use Conditionable;
    use HasMockClient;
    use Macroable;

    /**
     * The connector making the request.
     */
    protected Connector $connector;

    /**
     * The request used by the instance.
     */
    protected Request $request;

    /**
     * The method the request will use.
     */
    protected Method $method;

    /**
     * The URL the request will be made to.
     */
    protected string $url;

    /**
     * The body of the request.
     */
    protected ?BodyRepository $body = null;

    /**
     * The simulated response.
     */
    protected ?FakeResponse $fakeResponse = null;

    /**
     * Determine if the pending request is asynchronous
     */
    protected bool $asynchronous = false;

    /**
     * Build up the request payload.
     */
    public function __construct(Connector $connector, Request $request, MockClient $mockClient = null)
    {
        // Let's start by getting our PSR factory collection. This object contains all the
        // relevant factories for creating PSR-7 requests as well as URIs and streams.

        $this->factoryCollection = $connector->sender()->getFactoryCollection();

        // Now we'll set the base properties

        $this->connector = $connector;
        $this->request = $request;
        $this->method = $request->getMethod();
        $this->url = URLHelper::join($this->connector->resolveBaseUrl(), $this->request->resolveEndpoint());
        $this->authenticator = $request->getAuthenticator() ?? $connector->getAuthenticator();
        $this->mockClient = $mockClient ?? $request->getMockClient() ?? $connector->getMockClient() ?? MockClient::getGlobal();

        // Now, we'll register our global middleware and our mock response middleware.
        // Registering these middleware first means that the mock client can set
        // the fake response for every subsequent middleware.

        $this->middleware()->merge(Config::globalMiddleware());
        $this->middleware()->onRequest(new DetermineMockResponse, 'determineMockResponse');

        // Next, we'll boot our plugins. These plugins can add headers, config variables and
        // even register their own middleware. We'll use a tap method to simply apply logic
        // to the PendingRequest. After that, we will merge together our request properties
        // like headers, config, middleware, body and delay, and we'll follow it up by
        // invoking our authenticators. We'll do this here because when middleware is
        // executed, the developer will have access to any headers added by the middleware.

        $this
            ->tap(new BootPlugins)
            ->tap(new MergeRequestProperties)
            ->tap(new MergeBody)
            ->tap(new MergeDelay)
            ->tap(new AuthenticatePendingRequest)
            ->tap(new BootConnectorAndRequest);

        // Now, we'll register some default middleware for validating the request properties and
        // running the delay that should have been set by the user.

        $this->middleware()
            ->onRequest(new ValidateProperties, 'validateProperties')
            ->onRequest(new DelayMiddleware, 'delayMiddleware');

        // Finally, we will execute the request middleware pipeline which will
        // process the middleware in the order we added it.

        $this->middleware()->executeRequestPipeline($this);
    }

    /**
     * Authenticate the PendingRequest
     *
     * @return $this
     */
    public function authenticate(Authenticator $authenticator): static
    {
        $this->authenticator = $authenticator;

        // Since the PendingRequest has already been constructed we will run the set
        // method on the authenticator which runs it straight away.

        $this->authenticator->set($this);

        return $this;
    }

    /**
     * Execute the response pipeline.
     */
    public function executeResponsePipeline(Response $response): Response
    {
        return $this->middleware()->executeResponsePipeline($response);
    }

    /**
     * Execute the fatal pipeline.
     */
    public function executeFatalPipeline(FatalRequestException $throwable): void
    {
        $this->middleware()->executeFatalPipeline($throwable);
    }

    /**
     * Get the request.
     */
    public function getRequest(): Request
    {
        return $this->request;
    }

    /**
     * Get the connector.
     */
    public function getConnector(): Connector
    {
        return $this->connector;
    }

    /**
     * Get the URL of the request.
     */
    public function getUrl(): string
    {
        return $this->url;
    }

    /**
     * Set the URL of the PendingRequest
     *
     * Note: This will be combined with the query parameters to create
     * a UriInterface that will be passed to a PSR-7 request.
     *
     * @return $this
     */
    public function setUrl(string $url): static
    {
        $this->url = $url;

        return $this;
    }

    /**
     * Get the HTTP method used for the request
     */
    public function getMethod(): Method
    {
        return $this->method;
    }

    /**
     * Set the method of the PendingRequest
     *
     * @return $this
     */
    public function setMethod(Method $method): static
    {
        $this->method = $method;

        return $this;
    }

    /**
     * Retrieve the body on the instance
     */
    public function body(): ?BodyRepository
    {
        return $this->body;
    }

    /**
     * Set the body repository
     *
     * @return $this
     */
    public function setBody(?BodyRepository $body): static
    {
        $this->body = $body;

        return $this;
    }

    /**
     * Get the fake response
     */
    public function getFakeResponse(): ?FakeResponse
    {
        return $this->fakeResponse;
    }

    /**
     * Set the fake response
     *
     * @return $this
     */
    public function setFakeResponse(?FakeResponse $fakeResponse): static
    {
        $this->fakeResponse = $fakeResponse;

        return $this;
    }

    /**
     * Check if a fake response has been set
     */
    public function hasFakeResponse(): bool
    {
        return $this->fakeResponse instanceof FakeResponse;
    }

    /**
     * Check if the request is asynchronous
     */
    public function isAsynchronous(): bool
    {
        return $this->asynchronous;
    }

    /**
     * Set if the request is going to be sent asynchronously
     *
     * @return $this
     */
    public function setAsynchronous(bool $asynchronous): static
    {
        $this->asynchronous = $asynchronous;

        return $this;
    }

    /**
     * Get the response class
     *
     * @return class-string<\Saloon\Http\Response>
     * @throws \Saloon\Exceptions\InvalidResponseClassException
     */
    public function getResponseClass(): string
    {
        $response = $this->request->resolveResponseClass() ?? $this->connector->resolveResponseClass() ?? Response::class;

        if (! class_exists($response) || ! Helpers::isSubclassOf($response, Response::class)) {
            throw new InvalidResponseClassException;
        }

        return $response;
    }

    /**
     * Tap into the pending request
     *
     * @return $this
     */
    protected function tap(callable $callable): static
    {
        $callable($this);

        return $this;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Http/Request.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http;

use LogicException;
use Saloon\Enums\Method;
use Saloon\Traits\Bootable;
use Saloon\Traits\Makeable;
use Saloon\Traits\HasDebugging;
use Saloon\Traits\Conditionable;
use Saloon\Traits\HasMockClient;
use Saloon\Traits\HandlesPsrRequest;
use Saloon\Traits\ManagesExceptions;
use Saloon\Traits\Auth\AuthenticatesRequests;
use Saloon\Traits\RequestProperties\HasTries;
use Saloon\Traits\Responses\HasCustomResponses;
use Saloon\Traits\Request\CreatesDtoFromResponse;
use Saloon\Traits\RequestProperties\HasRequestProperties;

abstract class Request
{
    use CreatesDtoFromResponse;
    use AuthenticatesRequests;
    use HasRequestProperties;
    use HasCustomResponses;
    use ManagesExceptions;
    use HandlesPsrRequest;
    use HasMockClient;
    use Conditionable;
    use HasDebugging;
    use HasTries;
    use Bootable;
    use Makeable;

    /**
     * Define the HTTP method.
     */
    protected Method $method;

    /**
     * Get the method of the request.
     */
    public function getMethod(): Method
    {
        if (! isset($this->method)) {
            throw new LogicException('Your request is missing a HTTP method. You must add a method property like [protected Method $method = Method::GET]');
        }

        return $this->method;
    }

    /**
     * Define the endpoint for the request.
     */
    abstract public function resolveEndpoint(): string;
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Http/Senders/GuzzleSender.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http\Senders;

use Exception;
use Saloon\Config;
use Saloon\Http\Response;
use GuzzleHttp\HandlerStack;
use Saloon\Contracts\Sender;
use GuzzleHttp\RequestOptions;
use Saloon\Http\PendingRequest;
use GuzzleHttp\Psr7\HttpFactory;
use Saloon\Data\FactoryCollection;
use GuzzleHttp\Client as GuzzleClient;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\TransferException;
use Saloon\Exceptions\Request\FatalRequestException;
use Saloon\Http\Senders\Factories\GuzzleMultipartBodyFactory;

class GuzzleSender implements Sender
{
    /**
     * The Guzzle client.
     */
    protected GuzzleClient $client;

    /**
     * Guzzle's Handler Stack.
     */
    protected HandlerStack $handlerStack;

    /**
     * Constructor
     *
     * Create the HTTP client.
     */
    public function __construct()
    {
        $this->client = $this->createGuzzleClient();
    }

    /**
     * Get the factory collection
     */
    public function getFactoryCollection(): FactoryCollection
    {
        $factory = new HttpFactory;

        return new FactoryCollection(
            requestFactory: $factory,
            uriFactory: $factory,
            streamFactory: $factory,
            responseFactory: $factory,
            multipartBodyFactory: new GuzzleMultipartBodyFactory,
        );
    }

    /**
     * Create a new Guzzle client
     */
    protected function createGuzzleClient(): GuzzleClient
    {
        // We'll use HandlerStack::create as it will create a default
        // handler stack with the default Guzzle middleware like
        // http_errors, cookies etc.

        $this->handlerStack = HandlerStack::create();

        // Now we'll return new Guzzle client with some default request
        // options configured. We'll also define the handler stack we
        // created above. Since it's a property, developers may
        // customise or add middleware to the handler stack.

        return new GuzzleClient([
            RequestOptions::CRYPTO_METHOD => Config::$defaultTlsMethod,
            RequestOptions::CONNECT_TIMEOUT => Config::$defaultConnectionTimeout,
            RequestOptions::TIMEOUT => Config::$defaultRequestTimeout,
            RequestOptions::HTTP_ERRORS => true,
            'handler' => $this->handlerStack,
        ]);
    }

    /**
     * Send a synchronous request.
     *
     * @throws \GuzzleHttp\Exception\GuzzleException
     * @throws \Saloon\Exceptions\Request\FatalRequestException
     */
    public function send(PendingRequest $pendingRequest): Response
    {
        $request = $pendingRequest->createPsrRequest();
        $requestOptions = $pendingRequest->config()->all();

        try {
            $guzzleResponse = $this->client->send($request, $requestOptions);

            return $this->createResponse($guzzleResponse, $pendingRequest, $request);
        } catch (ConnectException $exception) {
            // ConnectException means a network exception has happened, like Guzzle
            // not being able to connect to the host.

            throw new FatalRequestException($exception, $pendingRequest);
        } catch (RequestException $exception) {
            // Sometimes, Guzzle will throw a RequestException without a response. This
            // means that it was fatal, so we should still throw a fatal request exception.

            $guzzleResponse = $exception->getResponse();

            if (is_null($guzzleResponse)) {
                throw new FatalRequestException($exception, $pendingRequest);
            }

            return $this->createResponse($guzzleResponse, $pendingRequest, $request, $exception);
        }
    }

    /**
     * Send an asynchronous request
     */
    public function sendAsync(PendingRequest $pendingRequest): PromiseInterface
    {
        $request = $pendingRequest->createPsrRequest();
        $requestOptions = $pendingRequest->config()->all();

        $promise = $this->client->sendAsync($request, $requestOptions);

        return $this->processPromise($request, $promise, $pendingRequest);
    }

    /**
     * Update the promise provided by Guzzle.
     */
    protected function processPromise(RequestInterface $psrRequest, PromiseInterface $promise, PendingRequest $pendingRequest): PromiseInterface
    {
        return $promise
            ->then(
                function (ResponseInterface $guzzleResponse) use ($psrRequest, $pendingRequest) {
                    // Instead of the promise returning a Guzzle response, we want to return
                    // a Saloon response.

                    return $this->createResponse($guzzleResponse, $pendingRequest, $psrRequest);
                },
                function (TransferException $guzzleException) use ($pendingRequest, $psrRequest) {
                    // When the exception wasn't a RequestException, we'll throw a fatal
                    // exception as this is likely a ConnectException, but it will
                    // catch any new ones Guzzle release.

                    if (! $guzzleException instanceof RequestException) {
                        throw new FatalRequestException($guzzleException, $pendingRequest);
                    }

                    // Sometimes, Guzzle will throw a RequestException without a response. This
                    // means that it was fatal, so we should still throw a fatal request exception.

                    $guzzleResponse = $guzzleException->getResponse();

                    if (is_null($guzzleResponse)) {
                        throw new FatalRequestException($guzzleException, $pendingRequest);
                    }

                    // Otherwise we'll create a response to convert into an exception.
                    // This will run the exception through the exception handlers
                    // which allows the user to handle their own exceptions.

                    $response = $this->createResponse($guzzleResponse, $pendingRequest, $psrRequest, $guzzleException);

                    // Throw the exception our way

                    return ($exception = $response->toException()) ? throw $exception : $response;
                }
            );
    }

    /**
     * Create a response.
     */
    protected function createResponse(ResponseInterface $psrResponse, PendingRequest $pendingRequest, RequestInterface $psrRequest, Exception $exception = null): Response
    {
        /** @var class-string<\Saloon\Http\Response> $responseClass */
        $responseClass = $pendingRequest->getResponseClass();

        return $responseClass::fromPsrResponse($psrResponse, $pendingRequest, $psrRequest, $exception);
    }

    /**
     * Add a middleware to the handler stack.
     *
     * @return $this
     */
    public function addMiddleware(callable $callable, string $name = ''): static
    {
        $this->handlerStack->push($callable, $name);

        return $this;
    }

    /**
     * Overwrite the entire handler stack.
     *
     * @return $this
     */
    public function setHandlerStack(HandlerStack $handlerStack): static
    {
        $this->handlerStack = $handlerStack;

        return $this;
    }

    /**
     * Get the handler stack.
     */
    public function getHandlerStack(): HandlerStack
    {
        return $this->handlerStack;
    }

    /**
     * Get the Guzzle client
     */
    public function getGuzzleClient(): GuzzleClient
    {
        return $this->client;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Http/Senders/Factories/GuzzleMultipartBodyFactory.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http\Senders\Factories;

use Saloon\Data\MultipartValue;
use GuzzleHttp\Psr7\MultipartStream;
use Psr\Http\Message\StreamInterface;
use Saloon\Contracts\MultipartBodyFactory;
use Psr\Http\Message\StreamFactoryInterface;

class GuzzleMultipartBodyFactory implements MultipartBodyFactory
{
    /**
     * Create a multipart body
     *
     * @param array<MultipartValue> $multipartValues
     */
    public function create(StreamFactoryInterface $streamFactory, array $multipartValues, string $boundary): StreamInterface
    {
        $elements = array_map(static function (MultipartValue $value) {
            return [
                'name' => $value->name,
                'filename' => $value->filename,
                'contents' => $value->value,
                'headers' => $value->headers,
            ];
        }, $multipartValues);

        return new MultipartStream($elements, $boundary);
    }
}

```````


`/home/ddebowczyk/projects/_php/saloon/src/Http/PendingRequest/AuthenticatePendingRequest.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http\PendingRequest;

use Saloon\Http\PendingRequest;
use Saloon\Contracts\Authenticator;

class AuthenticatePendingRequest
{
    /**
     * Authenticate the pending request
     */
    public function __invoke(PendingRequest $pendingRequest): PendingRequest
    {
        $authenticator = $pendingRequest->getAuthenticator();

        if ($authenticator instanceof Authenticator) {
            $authenticator->set($pendingRequest);
        }

        return $pendingRequest;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Http/PendingRequest/BootConnectorAndRequest.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http\PendingRequest;

use Saloon\Http\PendingRequest;

class BootConnectorAndRequest
{
    /**
     * Boot the connector and request
     */
    public function __invoke(PendingRequest $pendingRequest): PendingRequest
    {
        $pendingRequest->getConnector()->boot($pendingRequest);
        $pendingRequest->getRequest()->boot($pendingRequest);

        return $pendingRequest;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Http/PendingRequest/MergeBody.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http\PendingRequest;

use Saloon\Http\PendingRequest;
use Saloon\Contracts\Body\HasBody;
use Saloon\Contracts\Body\MergeableBody;
use Saloon\Exceptions\PendingRequestException;
use Saloon\Repositories\Body\MultipartBodyRepository;

class MergeBody
{
    /**
     * Merge connector and request body
     *
     * @throws \Saloon\Exceptions\PendingRequestException
     */
    public function __invoke(PendingRequest $pendingRequest): PendingRequest
    {
        $connector = $pendingRequest->getConnector();
        $request = $pendingRequest->getRequest();

        $connectorBody = $connector instanceof HasBody ? clone $connector->body() : null;
        $requestBody = $request instanceof HasBody ? clone $request->body() : null;

        if (is_null($connectorBody) && is_null($requestBody)) {
            return $pendingRequest;
        }

        // When both the connector and the request use the `HasBody` interface - we will enforce
        // that they are both of the same type. This means there won't be any confusion when
        // merging.

        if (isset($connectorBody, $requestBody) && ! $connectorBody instanceof $requestBody) {
            throw new PendingRequestException('Connector and request body types must be the same.');
        }

        // Now we'll look at both the request body and the connector body. If the request
        // body is null (not set) then we will use the connector body. If both are set
        // then the request body will still be preferred.

        $body = $requestBody ?? $connectorBody;

        // When both the connector and the request body repositories are mergeable then we
        // will merge them together.

        if (isset($connectorBody, $requestBody) && $connectorBody instanceof MergeableBody && $requestBody instanceof MergeableBody) {
            // We'll merge the request body into the connector body so any properties on the request
            // body will take priority if they are using a keyed array.

            $body = $connectorBody->merge($requestBody->all());
        }

        // Now we'll check if the body is a MultipartBodyRepository. If it is, then we must
        // set the body factory on the instance so the toStream method can create a stream
        // later on in the process.

        if ($body instanceof MultipartBodyRepository) {
            $body->setMultipartBodyFactory($pendingRequest->getFactoryCollection()->multipartBodyFactory);
        }

        $pendingRequest->setBody($body);

        return $pendingRequest;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Http/PendingRequest/MergeDelay.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http\PendingRequest;

use Saloon\Http\PendingRequest;

class MergeDelay
{
    /**
     * Merge connector and request delay
     */
    public function __invoke(PendingRequest $pendingRequest): PendingRequest
    {
        $connector = $pendingRequest->getConnector();
        $request = $pendingRequest->getRequest();

        $pendingRequest->delay()->set($connector->delay()->get());

        if ($request->delay()->isNotEmpty()) {
            $pendingRequest->delay()->set($request->delay()->get());
        }

        return $pendingRequest;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Http/PendingRequest/MergeRequestProperties.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http\PendingRequest;

use Saloon\Http\PendingRequest;

class MergeRequestProperties
{
    /**
     * Merge connector and request properties (headers, query, config, middleware)
     */
    public function __invoke(PendingRequest $pendingRequest): PendingRequest
    {
        $connector = $pendingRequest->getConnector();
        $request = $pendingRequest->getRequest();

        $pendingRequest->headers()->merge(
            $connector->headers()->all(),
            $request->headers()->all()
        );

        $pendingRequest->query()->merge(
            $connector->query()->all(),
            $request->query()->all()
        );

        $pendingRequest->config()->merge(
            $connector->config()->all(),
            $request->config()->all()
        );

        $pendingRequest->middleware()
            ->merge($connector->middleware())
            ->merge($request->middleware());

        return $pendingRequest;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Http/PendingRequest/BootPlugins.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http\PendingRequest;

use Saloon\Helpers\Helpers;
use Saloon\Http\PendingRequest;

class BootPlugins
{
    /**
     * Boot the plugins
     */
    public function __invoke(PendingRequest $pendingRequest): PendingRequest
    {
        $connector = $pendingRequest->getConnector();
        $request = $pendingRequest->getRequest();

        $connectorTraits = Helpers::classUsesRecursive($connector);
        $requestTraits = Helpers::classUsesRecursive($request);

        foreach ($connectorTraits as $connectorTrait) {
            Helpers::bootPlugin($pendingRequest, $connector, $connectorTrait);
        }

        foreach ($requestTraits as $requestTrait) {
            Helpers::bootPlugin($pendingRequest, $request, $requestTrait);
        }

        return $pendingRequest;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Http/SoloRequest.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http;

use Saloon\Traits\Request\HasConnector;
use Saloon\Http\Connectors\NullConnector;

abstract class SoloRequest extends Request
{
    use HasConnector;

    /**
     * Create a new connector instance.
     */
    protected function resolveConnector(): Connector
    {
        return new NullConnector;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Http/Pool.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http;

use Closure;
use Generator;
use GuzzleHttp\Promise\EachPromise;
use GuzzleHttp\Promise\PromiseInterface;
use Saloon\Exceptions\InvalidPoolItemException;

class Pool
{
    /**
     * Requests inside the pool
     *
     * @var iterable<\GuzzleHttp\Promise\PromiseInterface|\Saloon\Http\Request>
     */
    protected iterable $requests;

    /**
     * Handle Response Callback
     *
     * @var \Closure(\Saloon\Http\Response, array-key, \GuzzleHttp\Promise\PromiseInterface): (void)|null
     */
    protected ?Closure $responseHandler = null;

    /**
     * Handle Exception Callback
     *
     * @var \Closure(mixed, array-key, \GuzzleHttp\Promise\PromiseInterface): (void)|null
     */
    protected ?Closure $exceptionHandler = null;

    /**
     * Connector
     */
    protected Connector $connector;

    /**
     * Concurrency
     *
     * How many requests will be sent at once.
     *
     * @var int|\Closure(int): int
     */
    protected int|Closure $concurrency;

    /**
     * Constructor
     *
     * @param iterable<\GuzzleHttp\Promise\PromiseInterface|\Saloon\Http\Request>|callable(\Saloon\Http\Connector): iterable<\GuzzleHttp\Promise\PromiseInterface|\Saloon\Http\Request> $requests
     * @param int|callable(int $pendingRequests): (int) $concurrency
     * @param callable(\Saloon\Http\Response, array-key $key, \GuzzleHttp\Promise\PromiseInterface $poolAggregate): (void)|null $responseHandler
     * @param callable(mixed $reason, array-key $key, \GuzzleHttp\Promise\PromiseInterface $poolAggregate): (void)|null $exceptionHandler
     */
    public function __construct(Connector $connector, iterable|callable $requests = [], int|callable $concurrency = 5, callable|null $responseHandler = null, callable|null $exceptionHandler = null)
    {
        $this->connector = $connector;
        $this->setRequests($requests);
        $this->setConcurrency($concurrency);

        if (! is_null($responseHandler)) {
            $this->withResponseHandler($responseHandler);
        }

        if (! is_null($exceptionHandler)) {
            $this->withExceptionHandler($exceptionHandler);
        }
    }

    /**
     * Specify a callback to happen for each successful request
     *
     * @param callable(\Saloon\Http\Response, array-key $key, \GuzzleHttp\Promise\PromiseInterface $poolAggregate): (void) $callable
     * @return $this
     */
    public function withResponseHandler(callable $callable): static
    {
        $this->responseHandler = $callable(...);

        return $this;
    }

    /**
     * Specify a callback to happen for each failed request
     *
     * @param callable(mixed $reason, array-key $key, \GuzzleHttp\Promise\PromiseInterface $poolAggregate): (void) $callable
     * @return $this
     */
    public function withExceptionHandler(callable $callable): static
    {
        $this->exceptionHandler = $callable(...);

        return $this;
    }

    /**
     * Set the amount of concurrent requests that should be sent
     *
     * @param int|callable(int $pendingRequests): (int) $concurrency
     * @return $this
     */
    public function setConcurrency(int|callable $concurrency): static
    {
        $this->concurrency = is_callable($concurrency) ? $concurrency(...) : $concurrency;

        return $this;
    }

    /**
     * Set the requests
     *
     * @param iterable<\GuzzleHttp\Promise\PromiseInterface|\Saloon\Http\Request>|callable(\Saloon\Http\Connector): iterable<\GuzzleHttp\Promise\PromiseInterface|\Saloon\Http\Request> $requests
     * @return $this
     */
    public function setRequests(iterable|callable $requests): static
    {
        if (is_callable($requests)) {
            $requests = $requests($this->connector);
        }

        if (is_iterable($requests)) {
            $requests = static fn (): Generator => yield from $requests;
        }

        $this->requests = $requests();

        return $this;
    }

    /**
     * Get the request generator
     *
     * @return iterable<\GuzzleHttp\Promise\PromiseInterface|\Saloon\Http\Request>
     */
    public function getRequests(): iterable
    {
        return $this->requests;
    }

    /**
     * Send the pool and create a Promise
     */
    public function send(): PromiseInterface
    {
        // Iterate through the existing generator and "prepare" the requests.
        // If they are SaloonRequests then we should convert them into
        // promises.

        $preparedRequests = function (): Generator {
            foreach ($this->requests as $key => $request) {
                match (true) {
                    $request instanceof Request => yield $key => $this->connector->sendAsync($request),
                    $request instanceof PromiseInterface => yield $key => $request,
                    default => throw new InvalidPoolItemException
                };
            }
        };

        // Next we'll use an EachPromise which accepts an iterator of
        // requests and will process them as the concurrency we set.

        $eachPromise = new EachPromise($preparedRequests(), [
            'concurrency' => $this->concurrency,
            'fulfilled' => $this->responseHandler,
            'rejected' => $this->exceptionHandler,
        ]);

        return $eachPromise->promise();
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Http/Connectors/NullConnector.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http\Connectors;

use Saloon\Http\Connector;

class NullConnector extends Connector
{
    /**
     * Define the base URL of the API.
     */
    public function resolveBaseUrl(): string
    {
        return '';
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Http/BaseResource.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http;

class BaseResource
{
    /**
     * Constructor
     */
    public function __construct(readonly protected Connector $connector)
    {
        //
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Http/Response.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http;

use Throwable;
use LogicException;
use SimpleXMLElement;
use Saloon\Traits\Macroable;
use InvalidArgumentException;
use Saloon\Helpers\ArrayHelpers;
use Saloon\Helpers\ObjectHelpers;
use Saloon\XmlWrangler\XmlReader;
use Illuminate\Support\Collection;
use Saloon\Contracts\FakeResponse;
use Saloon\Repositories\ArrayStore;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\DomCrawler\Crawler;
use Saloon\Helpers\RequestExceptionHelper;
use Saloon\Contracts\DataObjects\WithResponse;
use Saloon\Contracts\ArrayStore as ArrayStoreContract;

class Response
{
    use Macroable;

    /**
     * The PSR request
     */
    protected readonly RequestInterface $psrRequest;

    /**
     * The PSR response from the sender.
     */
    protected readonly ResponseInterface $psrResponse;

    /**
     * The pending request that has all the request properties
     */
    protected readonly PendingRequest $pendingRequest;

    /**
     * The original sender exception
     */
    protected ?Throwable $senderException = null;

    /**
     * The decoded JSON response.
     *
     * @var array<array-key, mixed>
     */
    protected array $decodedJson;

    /**
     * The decoded JSON response object.
     */
    protected mixed $decodedJsonObject;

    /**
     * The decoded XML response.
     */
    protected string $decodedXml;

    /**
     * Denotes if the response has been mocked.
     */
    protected bool $mocked = false;

    /**
     * Denotes if the response has been cached.
     */
    protected bool $cached = false;

    /**
     * The simulated response payload if the response was simulated.
     */
    protected ?FakeResponse $fakeResponse = null;

    /**
     * Create a new response instance.
     */
    public function __construct(ResponseInterface $psrResponse, PendingRequest $pendingRequest, RequestInterface $psrRequest, Throwable $senderException = null)
    {
        $this->psrRequest = $psrRequest;
        $this->psrResponse = $psrResponse;
        $this->pendingRequest = $pendingRequest;
        $this->senderException = $senderException;
    }

    /**
     * Create a new response instance
     */
    public static function fromPsrResponse(ResponseInterface $psrResponse, PendingRequest $pendingRequest, RequestInterface $psrRequest, ?Throwable $senderException = null): static
    {
        return new static($psrResponse, $pendingRequest, $psrRequest, $senderException);
    }

    /**
     * Get the pending request that created the response.
     */
    public function getPendingRequest(): PendingRequest
    {
        return $this->pendingRequest;
    }

    /**
     * Get the connector that sent the request
     */
    public function getConnector(): Connector
    {
        return $this->pendingRequest->getConnector();
    }

    /**
     * Get the original request that created the response.
     */
    public function getRequest(): Request
    {
        return $this->pendingRequest->getRequest();
    }

    /**
     * Get the PSR-7 request
     */
    public function getPsrRequest(): RequestInterface
    {
        return $this->psrRequest;
    }

    /**
     * Create a PSR response from the raw response.
     */
    public function getPsrResponse(): ResponseInterface
    {
        return $this->psrResponse;
    }

    /**
     * Get the body of the response as string.
     */
    public function body(): string
    {
        $stream = $this->stream();

        $contents = $stream->getContents();

        if ($stream->isSeekable()) {
            $stream->rewind();
        }

        return $contents;
    }

    /**
     * Get the body as a stream.
     */
    public function stream(): StreamInterface
    {
        $stream = $this->psrResponse->getBody();

        if ($stream->isSeekable()) {
            $stream->rewind();
        }

        return $stream;
    }

    /**
     * Get the headers from the response.
     */
    public function headers(): ArrayStoreContract
    {
        $headers = array_map(static function (array $header) {
            return count($header) === 1 ? $header[0] : $header;
        }, $this->psrResponse->getHeaders());

        return new ArrayStore($headers);
    }

    /**
     * Get the status code of the response.
     */
    public function status(): int
    {
        return $this->psrResponse->getStatusCode();
    }

    /**
     * Get the original sender exception
     */
    public function getSenderException(): ?Throwable
    {
        return $this->senderException;
    }

    /**
     * Get the JSON decoded body of the response as an array or scalar value.
     *
     * @param array-key|null $key
     * @return ($key is null ? array<array-key, mixed> : mixed)
     */
    public function json(string|int|null $key = null, mixed $default = null): mixed
    {
        if (! isset($this->decodedJson)) {
            $this->decodedJson = json_decode($this->body() ?: '[]', true, 512, JSON_THROW_ON_ERROR);
        }

        if (is_null($key)) {
            return $this->decodedJson;
        }

        return ArrayHelpers::get($this->decodedJson, $key, $default);
    }

    /**
     * Get the JSON decoded body as an array. Provide a key to find a specific item in the JSON.
     *
     * Alias of json()
     *
     * @param array-key|null $key
     * @return ($key is null ? array<array-key, mixed> : mixed)
     */
    public function array(int|string|null $key = null, mixed $default = null): mixed
    {
        return $this->json($key, $default);
    }

    /**
     * Get the JSON decoded body of the response as an object or scalar value.
     */
    public function object(string|int|null $key = null, mixed $default = null): mixed
    {
        if (! isset($this->decodedJsonObject)) {
            $this->decodedJsonObject = json_decode($this->body() ?: '{}', false, 512, JSON_THROW_ON_ERROR);
        }

        if (is_null($key)) {
            return $this->decodedJsonObject;
        }

        return ObjectHelpers::get($this->decodedJsonObject, (string)$key, $default);
    }

    /**
     * Convert the XML response into a SimpleXMLElement.
     *
     * Suitable for reading small, simple XML responses but not suitable for
     * more advanced XML responses with namespaces and prefixes. Consider
     * using the xmlReader method instead for better compatibility.
     *
     * @see https://www.php.net/manual/en/book.simplexml.php
     */
    public function xml(mixed ...$arguments): SimpleXMLElement|bool
    {
        if (! isset($this->decodedXml)) {
            $this->decodedXml = $this->body();
        }

        return simplexml_load_string($this->decodedXml, ...$arguments);
    }

    /**
     * Load the XML response into a reader
     *
     * Suitable for reading large XML responses and supports a wider range of XML
     * documents. Requires XML Wrangler (composer require saloonphp/xml-wrangler)
     *
     * @see https://github.com/saloonphp/xml-wrangler
     */
    public function xmlReader(): XmlReader
    {
        return XmlReader::fromSaloonResponse($this);
    }

    /**
     * Get the JSON decoded body of the response as a collection.
     *
     * Requires Laravel Collections (composer require illuminate/collections)
     * @see https://github.com/illuminate/collections
     *
     * @param array-key|null $key
     * @return \Illuminate\Support\Collection<array-key, mixed>
     */
    public function collect(string|int|null $key = null): Collection
    {
        $data = $this->json($key);

        if (is_null($data)) {
            return Collection::empty();
        }

        if (is_array($data)) {
            return Collection::make($data);
        }

        return Collection::make([$data]);
    }

    /**
     * Cast the response to a DTO.
     */
    public function dto(): mixed
    {
        $request = $this->pendingRequest->getRequest();
        $connector = $this->pendingRequest->getConnector();

        $dataObject = $request->createDtoFromResponse($this) ?? $connector->createDtoFromResponse($this);

        if ($dataObject instanceof WithResponse) {
            $dataObject->setResponse($this);
        }

        return $dataObject;
    }

    /**
     * Convert the response into a DTO or throw a LogicException if the response failed
     */
    public function dtoOrFail(): mixed
    {
        if ($this->failed()) {
            throw new LogicException('Unable to create data transfer object as the response has failed.', 0, $this->toException());
        }

        return $this->dto();
    }

    /**
     * Parse the HTML or XML body into a Symfony DomCrawler instance.
     *
     * Requires Symfony Crawler (composer require symfony/dom-crawler)
     *
     * @see https://symfony.com/doc/current/components/dom_crawler.html
     */
    public function dom(): Crawler
    {
        return new Crawler($this->body());
    }

    /**
     * Convert the response to a data URL
     */
    public function dataUrl(): string
    {
        return 'data:'.$this->psrResponse->getHeaderLine('Content-Type').';base64,'.base64_encode($this->body());
    }

    /**
     * Determine if the request was successful.
     */
    public function successful(): bool
    {
        return $this->status() >= 200 && $this->status() < 300;
    }

    /**
     * Determine if the response code was "OK".
     */
    public function ok(): bool
    {
        return $this->status() === 200;
    }

    /**
     * Determine if the response was a redirect.
     */
    public function redirect(): bool
    {
        return $this->status() >= 300 && $this->status() < 400;
    }

    /**
     * Determine if the response indicates a client or server error occurred.
     */
    public function failed(): bool
    {
        $pendingRequest = $this->getPendingRequest();

        $requestFailedAccordingToConnector = $pendingRequest->getConnector()->hasRequestFailed($this);
        $requestFailedAccordingToRequest = $pendingRequest->getRequest()->hasRequestFailed($this);

        if ($requestFailedAccordingToRequest !== null || $requestFailedAccordingToConnector !== null) {
            return $requestFailedAccordingToRequest || $requestFailedAccordingToConnector;
        }

        return $this->serverError() || $this->clientError();
    }

    /**
     * Determine if the response indicates a client error occurred.
     */
    public function clientError(): bool
    {
        return $this->status() >= 400 && $this->status() < 500;
    }

    /**
     * Determine if the response indicates a server error occurred.
     */
    public function serverError(): bool
    {
        return $this->status() >= 500;
    }

    /**
     * Execute the given callback if there was a server or client error.
     *
     * @param callable($this): (void) $callback
     * @return $this
     */
    public function onError(callable $callback): static
    {
        if ($this->failed()) {
            $callback($this);
        }

        return $this;
    }

    /**
     * Determine if the response should throw a request exception.
     */
    public function shouldThrowRequestException(): bool
    {
        $pendingRequest = $this->getPendingRequest();

        return $pendingRequest->getRequest()->shouldThrowRequestException($this) || $pendingRequest->getConnector()->shouldThrowRequestException($this);
    }

    /**
     * Create an exception if a server or client error occurred.
     */
    public function toException(): ?Throwable
    {
        if (! $this->shouldThrowRequestException()) {
            return null;
        }

        return $this->createException();
    }

    /**
     * Create the request exception
     */
    protected function createException(): Throwable
    {
        $pendingRequest = $this->getPendingRequest();
        $senderException = $this->getSenderException();

        // We'll first check if the user has defined their own exception handlers.
        // We'll prioritise the request over the connector.

        $exception = $pendingRequest->getRequest()->getRequestException($this, $senderException) ?? $pendingRequest->getConnector()->getRequestException($this, $senderException);

        if ($exception instanceof Throwable) {
            return $exception;
        }

        // Otherwise, we'll throw our own request.

        return RequestExceptionHelper::create($this, $senderException);
    }

    /**
     * Throw an exception if a server or client error occurred.
     *
     * @return $this
     * @throws \Throwable
     */
    public function throw(): static
    {
        if ($this->shouldThrowRequestException()) {
            throw $this->toException();
        }

        return $this;
    }

    /**
     * Get a header from the response.
     *
     * @return string|array<array-key, mixed>|null
     */
    public function header(string $header): string|array|null
    {
        return $this->headers()->get($header);
    }

    /**
     * Create a temporary resource for the stream.
     *
     * Useful for storing the file. Make sure to close the raw stream after you have used it.
     *
     * @return resource
     */
    public function getRawStream(): mixed
    {
        $temporaryResource = fopen('php://temp', 'wb+');

        if ($temporaryResource === false) {
            throw new LogicException('Unable to create a temporary resource for the stream.');
        }

        $this->saveBodyToFile($temporaryResource, false);

        return $temporaryResource;
    }

    /**
     * Save the body to a file
     *
     * @param string|resource $resourceOrPath
     */
    public function saveBodyToFile(mixed $resourceOrPath, bool $closeResource = true): void
    {
        if (! is_string($resourceOrPath) && ! is_resource($resourceOrPath)) {
            throw new InvalidArgumentException('The $resourceOrPath argument must be either a file path or a resource.');
        }

        $resource = is_string($resourceOrPath) ? fopen($resourceOrPath, 'wb+') : $resourceOrPath;

        if ($resource === false) {
            throw new LogicException('Unable to open the resource.');
        }

        rewind($resource);

        $stream = $this->stream();

        while (! $stream->eof()) {
            fwrite($resource, $stream->read(1024));
        }

        rewind($resource);

        if ($closeResource === true) {
            fclose($resource);
        }
    }

    /**
     * Close the stream and any underlying resources.
     *
     * @return $this
     */
    public function close(): static
    {
        $this->stream()->close();

        return $this;
    }

    /**
     * Get the body of the response.
     */
    public function __toString(): string
    {
        return $this->body();
    }

    /**
     * Check if the response has been cached
     */
    public function isCached(): bool
    {
        return $this->cached;
    }

    /**
     * Check if the response has been mocked
     */
    public function isMocked(): bool
    {
        return $this->mocked;
    }

    /**
     * Check if the response has been simulated
     */
    public function isFaked(): bool
    {
        return $this->isMocked() || $this->isCached();
    }

    /**
     * Set if a response has been cached or not.
     *
     * @return $this
     */
    public function setCached(bool $value): static
    {
        $this->cached = true;

        return $this;
    }

    /**
     * Set if a response has been mocked or not.
     *
     * @return $this
     */
    public function setMocked(bool $value): static
    {
        $this->mocked = true;

        return $this;
    }

    /**
     * Set the simulated response payload if the response was simulated.
     *
     * @return $this
     */
    public function setFakeResponse(FakeResponse $fakeResponse): static
    {
        $this->fakeResponse = $fakeResponse;

        return $this;
    }

    /**
     * Get the simulated response payload if the response was simulated.
     */
    public function getFakeResponse(): ?FakeResponse
    {
        return $this->fakeResponse;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Http/Faking/MockClient.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Http\Faking;

use ReflectionClass;
use Saloon\Http\Request;
use Saloon\Http\Response;
use Saloon\Http\Connector;
use Saloon\Helpers\Helpers;
use Saloon\Helpers\URLHelper;
use Saloon\Http\PendingRequest;
use PHPUnit\Framework\Assert as PHPUnit;
use Saloon\Exceptions\NoMockResponseFoundException;
use Saloon\Exceptions\InvalidMockResponseCaptureMethodException;

class MockClient
{
    /**
     * Collection of all the responses that will be sequenced.
     *
     * @var array<\Saloon\Http\Faking\MockResponse|\Saloon\Http\Faking\Fixture|callable>
     */
    protected array $sequenceResponses = [];

    /**
     * Collection of responses used only when a connector is called.
     *
     * @var array<\Saloon\Http\Faking\MockResponse|\Saloon\Http\Faking\Fixture|callable>
     */
    protected array $connectorResponses = [];

    /**
     * Collection of responses used only when a request is called.
     *
     * @var array<\Saloon\Http\Faking\MockResponse|\Saloon\Http\Faking\Fixture|callable>
     */
    protected array $requestResponses = [];

    /**
     * Collection of responses that will run when the request is matched.
     *
     * @var array<\Saloon\Http\Faking\MockResponse|\Saloon\Http\Faking\Fixture|callable>
     */
    protected array $urlResponses = [];

    /**
     * Collection of all the recorded responses.
     *
     * @var array<\Saloon\Http\Faking\MockResponse|\Saloon\Http\Faking\Fixture|callable>
     */
    protected array $recordedResponses = [];

    /**
     * Global Mock Client
     *
     * Use MockClient::global() to register a global mock client
     */
    protected static ?MockClient $globalMockClient = null;

    /**
     * Constructor
     *
     * @param array<\Saloon\Http\Faking\MockResponse|\Saloon\Http\Faking\Fixture|callable> $mockData
     */
    public function __construct(array $mockData = [])
    {
        $this->addResponses($mockData);
    }

    /**
     * Store the mock responses in the correct places.
     *
     * @param array<\Saloon\Http\Faking\MockResponse|\Saloon\Http\Faking\Fixture|callable> $responses
     */
    public function addResponses(array $responses): void
    {
        foreach ($responses as $key => $response) {
            if (is_int($key)) {
                $key = null;
            }

            $this->addResponse($response, $key);
        }
    }

    /**
     * Add a mock response to the client
     */
    public function addResponse(MockResponse|Fixture|callable $response, ?string $captureMethod = null): void
    {
        if (is_null($captureMethod)) {
            $this->sequenceResponses[] = $response;

            return;
        }

        if (! is_string($captureMethod)) {
            throw new InvalidMockResponseCaptureMethodException;
        }

        // Let's detect if the capture method is either a connector or
        // a request. If so we'll put them in their designated arrays.

        if ($captureMethod && class_exists($captureMethod)) {
            $reflection = new ReflectionClass($captureMethod);

            if ($reflection->isSubclassOf(Connector::class)) {
                $this->connectorResponses[$captureMethod] = $response;

                return;
            }

            if ($reflection->isSubclassOf(Request::class)) {
                $this->requestResponses[$captureMethod] = $response;

                return;
            }
        }

        // Otherwise, the keys must be a URL.

        $this->urlResponses[$captureMethod] = $response;
    }

    /**
     * Get the next response in the sequence
     */
    public function getNextFromSequence(): mixed
    {
        return array_shift($this->sequenceResponses);
    }

    /**
     * Guess the next response based on the request.
     *
     * @throws \Saloon\Exceptions\NoMockResponseFoundException
     */
    public function guessNextResponse(PendingRequest $pendingRequest): MockResponse|Fixture
    {
        $request = $pendingRequest->getRequest();
        $requestClass = get_class($request);

        if (array_key_exists($requestClass, $this->requestResponses)) {
            return $this->mockResponseValue($this->requestResponses[$requestClass], $pendingRequest);
        }

        $connectorClass = get_class($pendingRequest->getConnector());

        if (array_key_exists($connectorClass, $this->connectorResponses)) {
            return $this->mockResponseValue($this->connectorResponses[$connectorClass], $pendingRequest);
        }

        $guessedResponse = $this->guessResponseFromUrl($pendingRequest);

        if (! is_null($guessedResponse)) {
            return $this->mockResponseValue($guessedResponse, $pendingRequest);
        }

        if (empty($this->sequenceResponses)) {
            throw new NoMockResponseFoundException($pendingRequest);
        }

        return $this->mockResponseValue($this->getNextFromSequence(), $pendingRequest);
    }

    /**
     * Guess the response from the URL.
     */
    private function guessResponseFromUrl(PendingRequest $pendingRequest): MockResponse|Fixture|callable|null
    {
        foreach ($this->urlResponses as $url => $response) {
            if (! URLHelper::matches($url, $pendingRequest->getUrl())) {
                continue;
            }

            return $response;
        }

        return null;
    }

    /**
     * Check if the responses are empty.
     */
    public function isEmpty(): bool
    {
        return empty($this->sequenceResponses) && empty($this->connectorResponses) && empty($this->requestResponses) && empty($this->urlResponses);
    }

    /**
     * Record a response.
     */
    public function recordResponse(Response $response): void
    {
        $this->recordedResponses[] = $response;
    }

    /**
     * Get all the recorded responses
     *
     * @return array<\Saloon\Http\Response>
     */
    public function getRecordedResponses(): array
    {
        return $this->recordedResponses;
    }

    /**
     * Get the last request that the mock manager sent.
     */
    public function getLastRequest(): ?Request
    {
        return $this->getLastResponse()?->getPendingRequest()->getRequest();
    }

    /**
     * Get the last request that the mock manager sent.
     */
    public function getLastPendingRequest(): ?PendingRequest
    {
        return $this->getLastResponse()?->getPendingRequest();
    }

    /**
     * Get the last response that the mock manager sent.
     */
    public function getLastResponse(): ?Response
    {
        if (empty($this->recordedResponses)) {
            return null;
        }

        $lastResponse = end($this->recordedResponses);

        reset($this->recordedResponses);

        return $lastResponse;
    }

    /**
     * Assert that a given request was sent.
     */
    public function assertSent(string|callable $value): void
    {
        $result = $this->checkRequestWasSent($value);

        PHPUnit::assertTrue($result, 'An expected request was not sent.');
    }

    /**
     * Assert that a given request was not sent.
     */
    public function assertNotSent(string|callable $request): void
    {
        $result = $this->checkRequestWasNotSent($request);

        PHPUnit::assertTrue($result, 'An unexpected request was sent.');
    }

    /**
     * Assert that given requests were sent in order
     *
     * @param array<\Closure|class-string<Request>|string> $callbacks
     */
    public function assertSentInOrder(array $callbacks): void
    {
        $this->assertSentCount(count($callbacks));

        foreach ($callbacks as $index => $callback) {
            $result = $this->checkRequestWasSent($callback, $index);

            PHPUnit::assertTrue($result, 'An expected request (#'.($index + 1).') was not sent.');
        }
    }

    /**
     * Assert JSON response data was received
     *
     * @deprecated This method will be removed in v4
     *
     * @param array<string, mixed> $data
     */
    public function assertSentJson(string $request, array $data): void
    {
        $this->assertSent(function ($currentRequest, $currentResponse) use ($request, $data) {
            return $currentRequest instanceof $request && $currentResponse->json() === $data;
        });
    }

    /**
     * Assert that nothing was sent.
     */
    public function assertNothingSent(): void
    {
        PHPUnit::assertEmpty($this->getRecordedResponses(), 'Requests were sent.');
    }

    /**
     * Assert a request count has been met.
     */
    public function assertSentCount(int $count, string $requestClass = null): void
    {
        if (is_string($requestClass)) {
            $actualCount = $this->getRequestSentCount()[$requestClass] ?? 0;

            PHPUnit::assertEquals($count, $actualCount);

            return;
        }

        PHPUnit::assertCount($count, $this->getRecordedResponses());
    }

    /**
     * Check if a given request was sent
     */
    protected function checkRequestWasSent(string|callable $request, ?int $index = null): bool
    {
        $passed = false;

        if (is_callable($request)) {
            return $this->checkClosureAgainstResponses($request, $index);
        }

        if (is_string($request)) {
            if (class_exists($request) && Helpers::isSubclassOf($request, Request::class)) {
                $passed = $this->findResponseByRequest($request, $index) instanceof Response;
            } else {
                $passed = $this->findResponseByRequestUrl($request, $index) instanceof Response;
            }
        }

        return $passed;
    }

    /**
     * Check if a request has not been sent.
     */
    protected function checkRequestWasNotSent(string|callable $request): bool
    {
        return ! $this->checkRequestWasSent($request);
    }

    /**
     * Assert a given request was sent.
     */
    public function findResponseByRequest(string $request, ?int $index = null): ?Response
    {
        if ($this->checkHistoryEmpty() === true) {
            return null;
        }

        if(! is_null($index)) {
            $recordedResponse = $this->getRecordedResponses()[$index];

            if ($recordedResponse->getPendingRequest()->getRequest() instanceof $request) {
                return $recordedResponse;
            }
        }

        $lastRequest = $this->getLastRequest();

        if ($lastRequest instanceof $request) {
            return $this->getLastResponse();
        }

        foreach ($this->getRecordedResponses() as $recordedResponse) {
            if ($recordedResponse->getPendingRequest()->getRequest() instanceof $request) {
                return $recordedResponse;
            }
        }

        return null;
    }

    /**
     * Find a request that matches a given url pattern
     */
    public function findResponseByRequestUrl(string $url, ?int $index = null): ?Response
    {
        if ($this->checkHistoryEmpty() === true) {
            return null;
        }

        if(! is_null($index)) {
            $response = $this->getRecordedResponses()[$index];
            $pendingRequest = $response->getPendingRequest();

            if (URLHelper::matches($url, $pendingRequest->getUrl())) {
                return $response;
            }

            return null;
        }

        $lastPendingRequest = $this->getLastPendingRequest();

        if ($lastPendingRequest instanceof PendingRequest && URLHelper::matches($url, $lastPendingRequest->getUrl())) {
            return $this->getLastResponse();
        }

        foreach ($this->getRecordedResponses() as $response) {
            $pendingRequest = $response->getPendingRequest();

            if (URLHelper::matches($url, $pendingRequest->getUrl())) {
                return $response;
            }
        }

        return null;
    }

    /**
     * Register a global mock client
     *
     * This will register a global mock client that is available throughout the
     * application's lifecycle. You should destroy the global mock client
     * after each test using MockClient::destroyGlobal().
     *
     * @param array<\Saloon\Http\Faking\MockResponse|\Saloon\Http\Faking\Fixture|callable> $mockData
     */
    public static function global(array $mockData = []): MockClient
    {
        return static::$globalMockClient ??= new static($mockData);
    }

    /**
     * Get the global mock client if it has been registered
     */
    public static function getGlobal(): ?MockClient
    {
        return static::$globalMockClient;
    }

    /**
     * Destroy the global mock client
     */
    public static function destroyGlobal(): void
    {
        static::$globalMockClient = null;
    }

    /**
     * Test if the closure can pass with the history.
     */
    private function checkClosureAgainstResponses(callable $closure, ?int $index = null): bool
    {
        if ($this->checkHistoryEmpty() === true) {
            return false;
        }

        if(! is_null($index)) {
            $response = $this->getRecordedResponses()[$index];
            $request = $response->getPendingRequest()->getRequest();

            return $closure($request, $response);
        }

        // Let's first check if the latest response resolves the callable
        // with a successful result.

        $lastResponse = $this->getLastResponse();

        if ($lastResponse instanceof Response) {
            $passed = $closure($lastResponse->getPendingRequest()->getRequest(), $lastResponse);

            if ($passed === true) {
                return true;
            }
        }

        // If it was not the previous response, we should iterate through each of the
        // responses and break out if we get a match.

        foreach ($this->getRecordedResponses() as $response) {
            $request = $response->getPendingRequest()->getRequest();

            $passed = $closure($request, $response);

            if ($passed === true) {
                return true;
            }
        }

        return false;
    }

    /**
     * Will return true if the history is empty.
     */
    private function checkHistoryEmpty(): bool
    {
        return count($this->recordedResponses) <= 0;
    }

    /**
     * Get the mock value.
     */
    private function mockResponseValue(MockResponse|Fixture|callable $mockable, PendingRequest $pendingRequest): MockResponse|Fixture
    {
        if ($mockable instanceof MockResponse) {
            return $mockable;
        }

        if ($mockable instanceof Fixture) {
            return $mockable;
        }

        return $mockable($pendingRequest);
    }

    /**
     * Get an array of requests recorded with their count
     *
     * @return array<class-string, int>
     */
    private function getRequestSentCount(): array
    {
        $requests = array_map(static function (Response $response) {
            return $response->getRequest()::class;
        }, $this->getRecordedResponses());

        return array_count_values($requests);
    }
}

```````


`/home/ddebowczyk/projects/_php/saloon/src/Data/FactoryCollection.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Data;

use Psr\Http\Message\UriFactoryInterface;
use Saloon\Contracts\MultipartBodyFactory;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseFactoryInterface;

class FactoryCollection
{
    /**
     * Constructor
     *
     * This class is used to collect all the different PSR and Saloon factories
     * together into one, simple class that can be defined by senders.
     */
    public function __construct(
        public readonly RequestFactoryInterface  $requestFactory,
        public readonly UriFactoryInterface      $uriFactory,
        public readonly StreamFactoryInterface   $streamFactory,
        public readonly ResponseFactoryInterface $responseFactory,
        public readonly MultipartBodyFactory     $multipartBodyFactory,
    ) {
        //
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Data/RecordedResponse.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Data;

use JsonSerializable;
use Saloon\Http\Response;
use Saloon\Http\Faking\MockResponse;

class RecordedResponse implements JsonSerializable
{
    /**
     * Constructor
     *
     * @param array<string, mixed> $headers
     */
    public function __construct(
        public int   $statusCode,
        public array $headers = [],
        public mixed $data = null,
    ) {
        //
    }

    /**
     * Create an instance from file contents
     *
     * @throws \JsonException
     */
    public static function fromFile(string $contents): static
    {
        /**
         * @param array{
         *     statusCode: int,
         *     headers: array<string, mixed>,
         *     data: mixed,
         * } $fileData
         */
        $fileData = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);

        $data = $fileData['data'];

        if (isset($fileData['encoding']) && $fileData['encoding'] === 'base64') {
            $data = base64_decode($data);
        }

        return new static(
            statusCode: $fileData['statusCode'],
            headers: $fileData['headers'],
            data: $data
        );
    }

    /**
     * Create an instance from a Response
     */
    public static function fromResponse(Response $response): static
    {
        return new static(
            statusCode: $response->status(),
            headers: $response->headers()->all(),
            data: $response->body(),
        );
    }

    /**
     * Encode the instance to be stored as a file
     *
     * @throws \JsonException
     */
    public function toFile(): string
    {
        return json_encode($this, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
    }

    /**
     * Create a mock response from the fixture
     */
    public function toMockResponse(): MockResponse
    {
        return new MockResponse($this->data, $this->statusCode, $this->headers);
    }

    /**
     * Define the JSON object if this class is converted into JSON
     *
     * @return array{
     *     statusCode: int,
     *     headers: array<string, mixed>,
     *     data: mixed,
     * }
     */
    public function jsonSerialize(): array
    {
        $response = [
            'statusCode' => $this->statusCode,
            'headers' => $this->headers,
            'data' => $this->data,
        ];

        if (mb_check_encoding($response['data'], 'UTF-8') === false) {
            $response['data'] = base64_encode($response['data']);
            $response['encoding'] = 'base64';
        }

        return $response;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Data/MultipartValue.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Data;

use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;

class MultipartValue
{
    /**
     * Constructor
     *
     * @param \Psr\Http\Message\StreamInterface|resource|string|int $value
     * @param array<string, mixed> $headers
     */
    public function __construct(
        public string  $name,
        public mixed   $value,
        public ?string $filename = null,
        public array   $headers = []
    ) {
        if (! $value instanceof StreamInterface && ! is_resource($value) && ! is_string($value) && ! is_numeric($value)) {
            throw new InvalidArgumentException(sprintf('The value property must be either a %s, resource, string or numeric.', StreamInterface::class));
        }
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Data/Pipe.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Data;

use Closure;
use Saloon\Enums\PipeOrder;

class Pipe
{
    /**
     * The callable inside the pipe
     */
    public readonly Closure $callable;

    /**
     * Constructor
     *
     * @param callable(mixed $payload): (mixed) $callable
     */
    public function __construct(
        callable                   $callable,
        public readonly ?string    $name = null,
        public readonly ?PipeOrder $order = null,
    ) {
        $this->callable = $callable(...);
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Repositories/IntegerStore.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Repositories;

use Saloon\Traits\Conditionable;
use Saloon\Contracts\IntegerStore as IntegerStoreContract;

class IntegerStore implements IntegerStoreContract
{
    use Conditionable;

    /**
     * store Data
     */
    protected ?int $data = null;

    /**
     * Constructor
     */
    public function __construct(?int $value = null)
    {
        $this->set($value);
    }

    /**
     * Set a value inside the store
     *
     * @return $this
     */
    public function set(?int $value): static
    {
        $this->data = $value;

        return $this;
    }

    /**
     * Retrieve all in the store
     */
    public function get(): ?int
    {
        return $this->data;
    }

    /**
     * Determine if the store is empty
     */
    public function isEmpty(): bool
    {
        return empty($this->data);
    }

    /**
     * Determine if the store is not empty
     */
    public function isNotEmpty(): bool
    {
        return ! $this->isEmpty();
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Repositories/ArrayStore.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Repositories;

use Saloon\Helpers\Helpers;
use Saloon\Traits\Conditionable;
use Saloon\Contracts\ArrayStore as ArrayStoreContract;

class ArrayStore implements ArrayStoreContract
{
    use Conditionable;

    /**
     * The repository's store
     *
     * @var array<string, mixed>
     */
    protected array $data = [];

    /**
     * Constructor
     *
     * @param array<string, mixed> $data
     */
    public function __construct(array $data = [])
    {
        $this->data = $data;
    }

    /**
     * Retrieve all the items.
     *
     * @return array<string, mixed>
     */
    public function all(): array
    {
        return $this->data;
    }

    /**
     * Retrieve a single item.
     */
    public function get(string $key, mixed $default = null): mixed
    {
        return $this->all()[$key] ?? $default;
    }

    /**
     * Overwrite the entire repository.
     *
     * @param array<string, mixed> $data
     * @return $this
     */
    public function set(array $data): static
    {
        $this->data = $data;

        return $this;
    }

    /**
     * Merge in other arrays.
     *
     * @param array<string, mixed> ...$arrays
     * @return $this
     */
    public function merge(array ...$arrays): static
    {
        $this->data = array_merge($this->data, ...$arrays);

        return $this;
    }

    /**
     * Add an item to the repository.
     *
     * @return $this
     */
    public function add(string $key, mixed $value): static
    {
        $this->data[$key] = Helpers::value($value);

        return $this;
    }

    /**
     * Remove an item from the store.
     *
     * @return $this
     */
    public function remove(string $key): static
    {
        unset($this->data[$key]);

        return $this;
    }

    /**
     * Determine if the store is empty
     *
     *
     * @phpstan-assert-if-false non-empty-array $this->data
     */
    public function isEmpty(): bool
    {
        return empty($this->data);
    }

    /**
     * Determine if the store is not empty
     *
     *
     * @phpstan-assert-if-true non-empty-array $this->data
     */
    public function isNotEmpty(): bool
    {
        return ! $this->isEmpty();
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Repositories/Body/FormBodyRepository.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Repositories\Body;

use Stringable;
use Saloon\Traits\Body\CreatesStreamFromString;

class FormBodyRepository extends ArrayBodyRepository implements Stringable
{
    use CreatesStreamFromString;

    /**
     * Convert into a string.
     */
    public function __toString(): string
    {
        return http_build_query($this->all());
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Repositories/Body/ArrayBodyRepository.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Repositories\Body;

use LogicException;
use InvalidArgumentException;
use Saloon\Traits\Conditionable;
use Psr\Http\Message\StreamInterface;
use Saloon\Contracts\Body\MergeableBody;
use Saloon\Contracts\Body\BodyRepository;
use Psr\Http\Message\StreamFactoryInterface;

class ArrayBodyRepository implements BodyRepository, MergeableBody
{
    use Conditionable;

    /**
     * Repository Data
     *
     * @var array<array-key, mixed>
     */
    protected array $data = [];

    /**
     * Constructor
     *
     * @param array<array-key, mixed> $value
     */
    public function __construct(array $value = [])
    {
        $this->set($value);
    }

    /**
     * Set a value inside the repository
     *
     * @param array<array-key, mixed> $value
     * @return $this
     */
    public function set(mixed $value): static
    {
        if (! is_array($value)) {
            throw new InvalidArgumentException('The value must be an array');
        }

        $this->data = $value;

        return $this;
    }

    /**
     * Merge another array into the repository
     *
     * @param array<array-key, mixed> ...$arrays
     * @return $this
     */
    public function merge(array ...$arrays): static
    {
        $this->data = array_merge($this->data, ...$arrays);

        return $this;
    }

    /**
     * Add an element to the repository.
     *
     * @param array-key|null $key
     * @return $this
     */
    public function add(string|int|null $key = null, mixed $value = null): static
    {
        isset($key)
            ? $this->data[$key] = $value
            : $this->data[] = $value;

        return $this;
    }

    /**
     * Get the raw data in the repository.
     *
     * @return array<mixed, mixed>
     */
    public function all(): array
    {
        return $this->data;
    }

    /**
     * Get a specific key of the array
     *
     * Alias of `all()`.
     *
     * @param array-key|null $key
     * @return ($key is null ? array<array-key, mixed> : mixed)
     */
    public function get(string|int|null $key = null, mixed $default = null): mixed
    {
        if (is_null($key)) {
            return $this->all();
        }

        return $this->all()[$key] ?? $default;
    }

    /**
     * Remove an item from the repository.
     *
     * @param array-key $key
     * @return $this
     */
    public function remove(string|int $key): static
    {
        unset($this->data[$key]);

        return $this;
    }

    /**
     * Determine if the repository is empty
     *
     *
     * @phpstan-assert-if-false non-empty-array $this->data
     */
    public function isEmpty(): bool
    {
        return empty($this->data);
    }

    /**
     * Determine if the repository is not empty
     *
     *
     * @phpstan-assert-if-true non-empty-array $this->data
     */
    public function isNotEmpty(): bool
    {
        return ! $this->isEmpty();
    }

    /**
     * Convert the body repository into a stream
     */
    public function toStream(StreamFactoryInterface $streamFactory): StreamInterface
    {
        throw new LogicException('Unable to create a stream directly from an array body repository.');
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Repositories/Body/StringBodyRepository.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Repositories\Body;

use Stringable;
use Saloon\Traits\Conditionable;
use Saloon\Contracts\Body\BodyRepository;
use Saloon\Traits\Body\CreatesStreamFromString;

class StringBodyRepository implements BodyRepository, Stringable
{
    use CreatesStreamFromString;
    use Conditionable;

    /**
     * Repository Data
     */
    protected ?string $data = null;

    /**
     * Constructor
     */
    public function __construct(string|null $value = null)
    {
        $this->set($value);
    }

    /**
     * Set a value inside the repository
     *
     * @param string|null $value
     * @return $this
     */
    public function set(mixed $value): static
    {
        $this->data = $value;

        return $this;
    }

    /**
     * Retrieve all in the repository
     */
    public function all(): ?string
    {
        return $this->data;
    }

    /**
     * Determine if the repository is empty
     */
    public function isEmpty(): bool
    {
        return empty($this->data);
    }

    /**
     * Determine if the repository is not empty
     */
    public function isNotEmpty(): bool
    {
        return ! $this->isEmpty();
    }

    /**
     * Convert the repository into a string
     */
    public function __toString(): string
    {
        return $this->all() ?? '';
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Repositories/Body/JsonBodyRepository.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Repositories\Body;

use Stringable;
use Saloon\Traits\Body\CreatesStreamFromString;

class JsonBodyRepository extends ArrayBodyRepository implements Stringable
{
    use CreatesStreamFromString;

    /**
     * JSON encoding flags
     *
     * Use a Bitmask to separate other flags. For example: JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR
     */
    protected int $jsonFlags = JSON_THROW_ON_ERROR;

    /**
     * Set the JSON encoding flags
     *
     * Must be a bitmask like: ->setJsonFlags(JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)
     *
     * @return $this
     */
    public function setJsonFlags(int $flags): static
    {
        $this->jsonFlags = $flags;

        return $this;
    }

    /**
     * Get the JSON encoding flags
     */
    public function getJsonFlags(): int
    {
        return $this->jsonFlags;
    }

    /**
     * Convert the body repository into a string.
     */
    public function __toString(): string
    {
        $json = json_encode($this->all(), $this->getJsonFlags());

        return $json === false ? '' : $json;
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Repositories/Body/StreamBodyRepository.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Repositories\Body;

use InvalidArgumentException;
use Saloon\Traits\Conditionable;
use Psr\Http\Message\StreamInterface;
use Saloon\Contracts\Body\BodyRepository;
use Psr\Http\Message\StreamFactoryInterface;

class StreamBodyRepository implements BodyRepository
{
    use Conditionable;

    /**
     * The stream body
     *
     * @var StreamInterface|resource|null
     */
    protected mixed $stream = null;

    /**
     * Constructor
     *
     * @param StreamInterface|resource|null $value
     */
    public function __construct(mixed $value = null)
    {
        $this->set($value);
    }

    /**
     * Set a value inside the repository
     *
     * @param StreamInterface|resource|null $value
     * @return $this
     */
    public function set(mixed $value): static
    {
        if (isset($value) && ! $value instanceof StreamInterface && ! is_resource($value)) {
            throw new InvalidArgumentException('The value must a resource or be an instance of ' . StreamInterface::class);
        }

        $this->stream = $value;

        return $this;
    }

    /**
     * Retrieve the stream from the repository
     */
    public function all(): mixed
    {
        return $this->stream;
    }

    /**
     * Retrieve the stream from the repository
     *
     * Alias of "all" method.
     */
    public function get(): mixed
    {
        return $this->all();
    }

    /**
     * Determine if the repository is empty
     */
    public function isEmpty(): bool
    {
        return is_null($this->stream);
    }

    /**
     * Determine if the repository is not empty
     */
    public function isNotEmpty(): bool
    {
        return ! $this->isEmpty();
    }

    /**
     * Convert the body repository into a stream
     */
    public function toStream(StreamFactoryInterface $streamFactory): StreamInterface
    {
        $stream = $this->stream;

        return $stream instanceof StreamInterface ? $stream : $streamFactory->createStreamFromResource($stream);
    }
}

```````

`/home/ddebowczyk/projects/_php/saloon/src/Repositories/Body/MultipartBodyRepository.php`:

```````php
<?php

declare(strict_types=1);

namespace Saloon\Repositories\Body;

use InvalidArgumentException;
use Saloon\Data\MultipartValue;
use Saloon\Traits\Conditionable;
use Saloon\Helpers\StringHelpers;
use Saloon\Exceptions\BodyException;
use Psr\Http\Message\StreamInterface;
use Saloon\Contracts\Body\MergeableBody;
use Saloon\Contracts\Body\BodyRepository;
use Saloon\Contracts\MultipartBodyFactory;
use Psr\Http\Message\StreamFactoryInterface;

class MultipartBodyRepository implements BodyRepository, MergeableBody
{
    use Conditionable;

    /**
     * Base Repository
     */
    protected ArrayBodyRepository $data;

    /**
     * The Multipart Boundary
     */
    protected string $boundary;

    /**
     * Multipart Body Factory
     */
    protected MultipartBodyFactory $multipartBodyFactory;

    /**
     * Constructor
     *
     * @param array<\Saloon\Data\MultipartValue> $value
     * @throws \Exception
     */
    public function __construct(array $value = [], string $boundary = null)
    {
        $this->data = new ArrayBodyRepository;
        $this->boundary = is_null($boundary) ? StringHelpers::random(40) : $boundary;

        $this->set($value);
    }

    /**
     * Set a value inside the repository
     *
     * @param array<\Saloon\Data\MultipartValue> $value
     * @return $this
     */
    public function set(mixed $value): static
    {
        if (! is_array($value)) {
            throw new InvalidArgumentException('The value must be an array');
        }

        $this->data->set(
            $this->parseMultipartArray($value)
        );

        return $this;
    }

    /**
     * Merge another array into the repository
     *
     * @param array<\Saloon\Data\MultipartValue> ...$arrays
     * @return $this
     */
    public function merge(array ...$arrays): static
    {
        $this->data->merge(...array_map(
            $this->parseMultipartArray(...),
            $arrays,
        ));

        return $this;
    }

    /**
     * Add an element to the repository.
     *
     * @param \Psr\Http\Message\StreamInterface|resource|string $contents
     * @param array<string, mixed> $headers
     * @return $this
     */
    public function add(string $name, mixed $contents, string $filename = null, array $headers = []): static
    {
        $this->attach(new MultipartValue($name, $contents, $filename, $headers));

        return $this;
    }

    /**
     * Attach a multipart file
     *
     * @return $this
     */
    public function attach(MultipartValue $file): static
    {
        $this->data->add(null, $file);

        return $this;
    }

    /**
     * Get the raw data in the repository.
     *
     * @return array<\Saloon\Data\MultipartValue>
     */
    public function all(): array
    {
        return $this->data->all();
    }

    /**
     * Get a specific key of the array
     *
     * @param array-key $key
     * @return MultipartValue|array<MultipartValue>
     */
    public function get(string|int $key, mixed $default = null): MultipartValue|array
    {
        $values = array_values(array_filter($this->all(), static function (MultipartValue $value) use ($key) {
            return $value->name === $key;
        }));

        if (count($values) === 0) {
            return $default;
        }

        if (count($values) === 1) {
            return $values[0];
        }

        return $values;
    }

    /**
     * Remove an item from the repository.
     *
     * @return $this
     */
    public function remove(string $key): static
    {
        $values = array_filter($this->all(), static function (MultipartValue $value) use ($key) {
            return $value->name !== $key;
        });

        $this->set($values);

        return $this;
    }

    /**
     * Determine if the repository is empty
     */
    public function isEmpty(): bool
    {
        return $this->data->isEmpty();
    }

    /**
     * Determine if the repository is not empty
     */
    public function isNotEmpty(): bool
    {
        return $this->data->isNotEmpty();
    }

    /**
     * Parse a multipart array
     *
     * @param array<string, mixed> $value
     * @return array<\Saloon\Data\MultipartValue>
     */
    protected function parseMultipartArray(array $value): array
    {
        $multipartValues = array_filter($value, static fn (mixed $item): bool => $item instanceof MultipartValue);

        if (count($value) !== count($multipartValues)) {
            throw new InvalidArgumentException(sprintf('The value array must only contain %s objects.', MultipartValue::class));
        }

        return array_values($value);
    }

    /**
     * Set the multipart body factory
     */
    public function setMultipartBodyFactory(MultipartBodyFactory $multipartBodyFactory): MultipartBodyRepository
    {
        $this->multipartBodyFactory = $multipartBodyFactory;

        return $this;
    }

    /**
     * Get the boundary
     */
    public function getBoundary(): string
    {
        return $this->boundary;
    }

    /**
     * Convert the body repository into a stream
     *
     * @throws BodyException
     */
    public function toStream(StreamFactoryInterface $streamFactory): StreamInterface
    {
        if (! isset($this->multipartBodyFactory)) {
            throw new BodyException('Unable to create a multipart body stream because the multipart body factory was not set.');
        }

        return $this->multipartBodyFactory->create($streamFactory, $this->all(), $this->getBoundary());
    }
}

```````