designcise/bitframe

View on GitHub
src/Http/ServerRequestBuilder.php

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
<?php

/**
 * BitFrame Framework (https://www.bitframephp.com)
 *
 * @author    Daniyal Hamid
 * @copyright Copyright (c) 2017-2023 Daniyal Hamid (https://designcise.com)
 * @license   https://bitframephp.com/about/license MIT License
 */

declare(strict_types=1);

namespace BitFrame\Http;

use BitFrame\Http\Normalizer\UploadedFilesNormalizer;
use BitFrame\Http\Parser\MediaParserNegotiator;
use BitFrame\Http\Parser\{UriParser, HttpCookieParser};
use Psr\Http\Message\{
    RequestFactoryInterface,
    ResponseFactoryInterface,
    ServerRequestFactoryInterface,
    StreamFactoryInterface,
    UploadedFileFactoryInterface,
    UriFactoryInterface,
    ServerRequestInterface,
    StreamInterface,
};
use InvalidArgumentException;
use UnexpectedValueException;

use function array_keys;
use function is_array;
use function is_resource;
use function is_object;
use function strtr;
use function strtolower;
use function str_replace;
use function preg_match_all;
use function count;
use function sprintf;
use function implode;

use const PREG_SET_ORDER;

/**
 * Builds a server request.
 */
class ServerRequestBuilder
{
    /** @var callable */
    private static $preferredMediaParser = MediaParserNegotiator::class;

    private ServerRequestInterface $request;

    private ?StreamInterface $body = null;

    private object|null|array $parsedBody;

    public function __construct(
        private readonly array $server,
        private readonly RequestFactoryInterface
        & ResponseFactoryInterface
        & ServerRequestFactoryInterface
        & StreamFactoryInterface
        & UploadedFileFactoryInterface
        & UriFactoryInterface $factory,
    ) {
        $this->request = $factory->createServerRequest('GET', '/', $server);
    }

    public function build(): ServerRequestInterface
    {
        if (empty($this->parsedBody) && ! empty((string) $this->body)) {
            $parser = new self::$preferredMediaParser($this->request);

            $this->request = $this->request
                ->withParsedBody($parser->parse((string) $this->body));
        }

        return $this->request;
    }

    public function addMethod(): self
    {
        if (! empty($this->server['REQUEST_METHOD'])) {
            $this->request = $this->request->withMethod($this->server['REQUEST_METHOD']);
        }

        return $this;
    }

    public function addUri(): self
    {
        [$uri, $queryParams] = UriParser::parse($this->server);

        $this->request = $this->request
            ->withUri($this->factory->createUri($uri))
            ->withQueryParams($queryParams);

        return $this;
    }

    /**
     * @return $this
     *
     * @throws UnexpectedValueException
     */
    public function addProtocolVersion(): self
    {
        $protocolVer = '1.1';

        if (
            isset($this->server['SERVER_PROTOCOL'])
            && $this->server['SERVER_PROTOCOL'] !== "HTTP/$protocolVer"
        ) {
            $protocolVer = strtr((string) $this->server['SERVER_PROTOCOL'], ['HTTP/' => '']);

            $isNumeric = (int) $protocolVer;

            if (! $isNumeric) {
                throw new UnexpectedValueException(sprintf('Unrecognized protocol version "%s"', $protocolVer));
            }
        }

        $this->request = $this->request->withProtocolVersion($protocolVer);

        return $this;
    }

    public function addHeaders(): self
    {
        $pattern = '/(REDIRECT_)?(HTTP_|CONTENT_)([^ ]*)/i';
        $str = implode(' ', array_keys($this->server));
        preg_match_all($pattern, $str, $originalHeaders, PREG_SET_ORDER);

        $pattern = '/(redirect-)?(?:http-|(content-))([^ ]*)/';
        $str = strtolower(str_replace('_', '-', $str));
        preg_match_all($pattern, $str, $normalizedHeaders, PREG_SET_ORDER);

        $totalMatches = count($normalizedHeaders);

        for ($i = 0; $i < $totalMatches; $i++) {
            $isRedirect = ! (empty($normalizedHeaders[$i][1]));
            $originalKey = $originalHeaders[$i][2] . $originalHeaders[$i][3];

            // apache prefixes environment variables with `REDIRECT_` if they are
            // added by rewrite rules
            if ($isRedirect) {
                if (isset($this->server[$originalKey])) {
                    continue;
                }

                $originalKey = 'REDIRECT_' . $originalKey;
            }

            $newKey = $normalizedHeaders[$i][2] . $normalizedHeaders[$i][3];

            $this->request = $this->request
                ->withHeader($newKey, $this->server[$originalKey]);
        }

        return $this;
    }

    public function addCookieParams(array $cookies): self
    {
        if ($cookies === [] && isset($this->server['HTTP_COOKIE'])) {
            $cookies = HttpCookieParser::parse($this->server['HTTP_COOKIE']);
        }

        $this->request = $this->request
            ->withCookieParams($cookies ?: []);

        return $this;
    }

    public function addParsedBody(object|array|null $parsedBody): self
    {
        $this->request = $this->request
            ->withParsedBody($parsedBody);

        $this->parsedBody = $parsedBody;

        return $this;
    }

    /**
     * @param string|resource|StreamInterface $body
     *
     * @return $this
     */
    public function addBody($body): self
    {
        if (! $body instanceof StreamInterface) {
            if (is_resource($body)) {
                $body = $this->factory->createStreamFromResource($body);
            } elseif (! is_array($body) && ! is_object($body)) {
                $body = $this->factory->createStream((string) $body);
            }
        }

        if ($body instanceof StreamInterface && (string) $body !== '') {
            $this->request = $this->request->withBody($body);
            $this->body = $body;
        }

        return $this;
    }

    /**
     * Transforms each value into an `UploadedFile` instance, and ensures that nested
     * arrays are normalized.
     *
     * @param array $files
     *
     * @return $this
     *
     * @throws InvalidArgumentException
     */
    public function addUploadedFiles(array $files): self
    {
        $uploadedFilesNormalizer = new UploadedFilesNormalizer($this->factory);
        $this->request = $this->request
            ->withUploadedFiles($uploadedFilesNormalizer->normalize($files));

        return $this;
    }
}