RequestTrait.php

PHP

Path: src/Http/Message/RequestTrait.php

<?php
namespace mini\Http\Message;

use Psr\Http\Message\{
    RequestInterface,
    UriInterface
};

/**
 * Representation of an outgoing, client-side request.
 *
 * Per the HTTP specification, this interface includes properties for
 * each of the following:
 *
 * - Protocol version
 * - HTTP method
 * - URI
 * - Headers
 * - Message body
 *
 * During construction, implementations MUST attempt to set the Host header from
 * a provided URI if no Host header is provided.
 *
 * Requests are considered immutable; all methods that might change state MUST
 * be implemented such that they retain the internal state of the current
 * message and return an instance that contains the changed state.
 */
trait RequestTrait {
    use MessageTrait;

    protected string $method;
    protected string $requestTarget;
    protected ?UriInterface $uriOverride = null;

    public function __clone() {
        if ($this->uriOverride !== null) {
            $this->uriOverride = clone $this->uriOverride;
        }
    }

    /**
     * Configure the request.
     *
     * @param string $method                        Case-sensitive method.
     * @param string $requestTarget                 Request target (e.g., "/path?query=value")
     * @param string|resource|StreamInterface $body Body
     * @param string[][] $headers                   Array of header names => values
     * @param string $protocolVersion               The HTTP protocol version, typically "1.1" or "1.0"
     */
    protected function RequestTrait(string $method, string $requestTarget, mixed $body, array $headers=[], string $protocolVersion='1.1') {
        $this->method = $method;
        $this->requestTarget = $requestTarget;

        $this->MessageTrait(Stream::create($body), $headers, $protocolVersion);
    }

    /**
     * Retrieves the message's request target.
     *
     * Retrieves the message's request-target either as it will appear (for
     * clients), as it appeared at request (for servers), or as it was
     * specified for the instance (see withRequestTarget()).
     *
     * The request target is stored directly and returned as-is. This method
     * does not construct the target from the URI.
     *
     * @return string
     */
    public function getRequestTarget(): string {
        return $this->requestTarget;
    }

    /**
     * Return an instance with the specific request-target.
     *
     * If the request needs a non-origin-form request-target — e.g., for
     * specifying an absolute-form, authority-form, or asterisk-form —
     * this method may be used to create an instance with the specified
     * request-target, verbatim.
     *
     * This method MUST be implemented in such a way as to retain the
     * immutability of the message, and MUST return an instance that has the
     * changed request target.
     *
     * @see http://tools.ietf.org/html/rfc7230#section-5.3 (for the various
     *     request-target forms allowed in request messages)
     * @param string $requestTarget
     * @return static
     */
    public function withRequestTarget(string $requestTarget): RequestInterface {
        $c = clone $this;
        $c->requestTarget = $requestTarget;
        return $c;
    }

    /**
     * Retrieves the HTTP method of the request.
     *
     * @return string Returns the request method.
     */
    public function getMethod(): string {
        return $this->method;
    }

    /**
     * Return an instance with the provided HTTP method.
     *
     * While HTTP method names are typically all uppercase characters, HTTP
     * method names are case-sensitive and thus implementations SHOULD NOT
     * modify the given string.
     *
     * This method MUST be implemented in such a way as to retain the
     * immutability of the message, and MUST return an instance that has the
     * changed request method.
     *
     * @param string $method Case-sensitive method.
     * @return static
     * @throws \InvalidArgumentException for invalid HTTP methods.
     */
    public function withMethod(string $method): RequestInterface {
        $c = clone $this;
        $c->method = $method;
        return $c;
    }

    /**
     * Retrieves the URI instance.
     *
     * This method MUST return a UriInterface instance.
     *
     * If a URI override was set via withUri(), returns that instance.
     * Otherwise, constructs a new URI from the request target and Host header.
     *
     * Returns a relative URI if no Host header is present.
     *
     * @see http://tools.ietf.org/html/rfc3986#section-4.3
     * @return UriInterface Returns a UriInterface instance
     *     representing the URI of the request.
     */
    public function getUri(): UriInterface {
        if ($this->uriOverride !== null) {
            return $this->uriOverride;
        }

        // Construct from request target + headers
        $uri = $this->requestTarget;

        // Add scheme and host if Host header exists
        if ($host = $this->getHeaderLine('Host')) {
            // Default to http:// scheme (ServerRequest overrides this with HTTPS detection)
            $uri = "http://{$host}{$this->requestTarget}";
        }
        // else: return relative URI (just request target)

        return new Uri($uri);
    }

    /**
     * Returns an instance with the provided URI.
     *
     * This method MUST update the Host header of the returned request by
     * default if the URI contains a host component. If the URI does not
     * contain a host component, any pre-existing Host header MUST be carried
     * over to the returned request.
     *
     * You can opt-in to preserving the original state of the Host header by
     * setting `$preserveHost` to `true`. When `$preserveHost` is set to
     * `true`, this method interacts with the Host header in the following ways:
     *
     * - If the Host header is missing or empty, and the new URI contains
     *   a host component, this method MUST update the Host header in the returned
     *   request.
     * - If the Host header is missing or empty, and the new URI does not contain a
     *   host component, this method MUST NOT update the Host header in the returned
     *   request.
     * - If a Host header is present and non-empty, this method MUST NOT update
     *   the Host header in the returned request.
     *
     * This method MUST be implemented in such a way as to retain the
     * immutability of the message, and MUST return an instance that has the
     * new UriInterface instance.
     *
     * @see http://tools.ietf.org/html/rfc3986#section-4.3
     * @param UriInterface $uri New request URI to use.
     * @param bool $preserveHost Preserve the original state of the Host header.
     * @return static
     */
    public function withUri(UriInterface $uri, bool $preserveHost = false): RequestInterface {
        $host = $uri->getHost();
        if (($preserveHost && $this->hasHeader('host')) || !$host) {
            $c = clone $this;
        } else {
            $c = $this->withHeader('Host', $host);
        }
        $c->uriOverride = clone $uri;
        return $c;
    }

    /**
     * Get the query string from the request target.
     *
     * Returns the query string portion of the request target (everything after '?').
     * If a URI override is set via withUri(), returns the query from that URI instead.
     *
     * @return string Query string (without leading '?'), or empty string if no query
     */
    public function getQuery(): string {
        if ($this->uriOverride !== null) {
            return $this->uriOverride->getQuery();
        }

        $target = $this->requestTarget;
        if (str_contains($target, '?')) {
            return substr($target, strpos($target, '?') + 1);
        }

        return '';
    }
}