RequestGlobalProxy.php

PHP

Path: src/Http/RequestGlobalProxy.php

<?php

namespace mini\Http;

use mini\Mini;
use Psr\Http\Message\ServerRequestInterface;

/**
 * ArrayAccess proxy for request globals ($_GET, $_POST, $_COOKIE)
 *
 * Transparently proxies array access to PSR-7 ServerRequest methods, enabling
 * fiber-safe concurrent request handling without code changes.
 *
 * Traditional PHP behavior:
 * ```php
 * $id = $_GET['id'];           // Direct superglobal access
 * $name = $_POST['name'];      // Process-wide variables
 * ```
 *
 * With RequestGlobalProxy:
 * ```php
 * $_GET = new RequestGlobalProxy('query');     // Install once at startup
 * $_POST = new RequestGlobalProxy('post');
 *
 * // User code unchanged - transparently uses current request
 * $id = $_GET['id'];           // → gets from current ServerRequest
 * $name = $_POST['name'];      // → gets from current ServerRequest
 * ```
 *
 * Benefits:
 * - Zero code changes needed - existing $_GET['id'] works unchanged
 * - Fiber-safe by default - each fiber gets its own request context
 * - Works with all SAPIs (FPM, CGI, mod_php, Swoole, ReactPHP, phasync)
 * - Consistent architecture - no special adapters needed
 *
 * Limitations:
 * - Cannot modify request globals ($_GET['x'] = 'y' throws exception)
 * - Use PSR-7 withQueryParams() to modify request instead
 * - is_array($_GET) returns false (use isset(), array access works fine)
 *
 * @implements \ArrayAccess<string, mixed>
 * @implements \IteratorAggregate<string, mixed>
 */
class RequestGlobalProxy implements \ArrayAccess, \Countable, \IteratorAggregate
{
    /**
     * @param string $source Source type: 'query' ($_GET), 'post' ($_POST), 'cookie' ($_COOKIE)
     */
    public function __construct(
        private readonly string $source
    ) {}

    /**
     * Get value from current request context
     *
     * For $_GET proxy: integer keys (0, 1, 2, ...) fetch from 'mini.pathcomponents'
     * request attribute, which contains captured wildcard URL segments from routing.
     */
    public function offsetGet(mixed $offset): mixed
    {
        // Integer keys on $_GET come from path component wildcards
        if ($this->source === 'query' && is_int($offset)) {
            try {
                $request = Mini::$mini->get(ServerRequestInterface::class);
                $pathComponents = $request->getAttribute('mini.pathcomponents', []);
                return $pathComponents[$offset] ?? null;
            } catch (\Throwable $e) {
                return null;
            }
        }

        $data = $this->getData();
        return $data[$offset] ?? null;
    }

    /**
     * Check if key exists in current request context
     *
     * For $_GET proxy: integer keys (0, 1, 2, ...) check 'mini.pathcomponents'
     * request attribute, which contains captured wildcard URL segments from routing.
     */
    public function offsetExists(mixed $offset): bool
    {
        // Integer keys on $_GET come from path component wildcards
        if ($this->source === 'query' && is_int($offset)) {
            try {
                $request = Mini::$mini->get(ServerRequestInterface::class);
                $pathComponents = $request->getAttribute('mini.pathcomponents', []);
                return isset($pathComponents[$offset]);
            } catch (\Throwable $e) {
                return false;
            }
        }

        $data = $this->getData();
        return isset($data[$offset]);
    }

    /**
     * Setting values not supported - use PSR-7 methods instead
     *
     * @throws \RuntimeException
     */
    public function offsetSet(mixed $offset, mixed $value): void
    {
        throw new \RuntimeException(
            'Cannot modify request globals directly. ' .
            'Use PSR-7 methods: $request->withQueryParams(), ->withParsedBody(), etc.'
        );
    }

    /**
     * Unsetting values not supported - use PSR-7 methods instead
     *
     * @throws \RuntimeException
     */
    public function offsetUnset(mixed $offset): void
    {
        throw new \RuntimeException(
            'Cannot modify request globals directly. ' .
            'Use PSR-7 methods: $request->withQueryParams(), ->withParsedBody(), etc.'
        );
    }

    /**
     * Count elements in current request context
     */
    public function count(): int
    {
        return count($this->getData());
    }

    /**
     * Get iterator for current request context
     *
     * @return \ArrayIterator<string, mixed>
     */
    public function getIterator(): \ArrayIterator
    {
        return new \ArrayIterator($this->getData());
    }

    /**
     * Get data from current request's ServerRequest
     *
     * @return array<string, mixed>
     */
    private function getData(): array
    {
        try {
            $request = Mini::$mini->get(ServerRequestInterface::class);
        } catch (\Throwable $e) {
            // No request available yet - return empty array
            // This can happen during bootstrap before dispatch() is called
            return [];
        }

        return match($this->source) {
            'query' => $request->getQueryParams(),
            'post' => $request->getParsedBody() ?: [],
            'cookie' => $request->getCookieParams(),
            default => throw new \RuntimeException("Invalid request global source: {$this->source}"),
        };
    }

    /**
     * Debug info for var_dump()
     *
     * @return array<string, mixed>
     */
    public function __debugInfo(): array
    {
        return [
            'source' => $this->source,
            'data' => $this->getData(),
        ];
    }
}