diff --git a/CHANGELOG.md b/CHANGELOG.md index a04a19e..595c0a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ ## 3.0.0 under development -- Chg #76: Allow to use any PSR logger, `NullLogger` by default (@vjik) +- Chg #76: Allow to use any PSR logger, `NullLogger` by default (@vjik) +- Chg #77: Remove `ServerRequestFactory` (@vjik) +- Chg #77: Mark `SapiEmitter` as internal (@vjik) ## 2.3.0 March 10, 2024 diff --git a/src/SapiEmitter.php b/src/SapiEmitter.php index c9c8b8a..439797b 100644 --- a/src/SapiEmitter.php +++ b/src/SapiEmitter.php @@ -16,6 +16,8 @@ /** * `SapiEmitter` sends a response using standard PHP Server API i.e. with {@see header()} and "echo". + * + * @internal */ final class SapiEmitter { diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php deleted file mode 100644 index 2e996d7..0000000 --- a/src/ServerRequestFactory.php +++ /dev/null @@ -1,291 +0,0 @@ - $_SERVER */ - return $this->createFromParameters( - $_SERVER, - $this->getHeadersFromGlobals(), - $_COOKIE, - $_GET, - $_POST, - $_FILES, - fopen('php://input', 'rb') ?: null - ); - } - - /** - * Creates an instance of a server request from custom parameters. - * - * @param resource|StreamInterface|string|null $body - * - * @psalm-param array $server - * @psalm-param array $headers - * @psalm-param mixed $body - * - * @return ServerRequestInterface The server request instance. - */ - public function createFromParameters( - array $server, - array $headers = [], - array $cookies = [], - array $get = [], - array $post = [], - array $files = [], - mixed $body = null - ): ServerRequestInterface { - $method = $server['REQUEST_METHOD'] ?? null; - - if ($method === null) { - throw new RuntimeException('Unable to determine HTTP request method.'); - } - - $uri = $this->getUri($server); - $request = $this->serverRequestFactory->createServerRequest($method, $uri, $server); - - foreach ($headers as $name => $value) { - if ($name === 'Host' && $request->hasHeader('Host')) { - continue; - } - - $request = $request->withAddedHeader($name, $value); - } - - $protocol = '1.1'; - if (array_key_exists('SERVER_PROTOCOL', $server) && $server['SERVER_PROTOCOL'] !== '') { - $protocol = str_replace('HTTP/', '', $server['SERVER_PROTOCOL']); - } - - $request = $request - ->withProtocolVersion($protocol) - ->withQueryParams($get) - ->withParsedBody($post) - ->withCookieParams($cookies) - ->withUploadedFiles($this->getUploadedFilesArray($files)) - ; - - if ($body === null) { - return $request; - } - - if ($body instanceof StreamInterface) { - return $request->withBody($body); - } - - if (is_string($body)) { - return $request->withBody($this->streamFactory->createStream($body)); - } - - if (is_resource($body)) { - return $request->withBody($this->streamFactory->createStreamFromResource($body)); - } - - throw new InvalidArgumentException( - 'Body parameter for "ServerRequestFactory::createFromParameters()"' - . 'must be instance of StreamInterface, resource or null.', - ); - } - - /** - * @psalm-param array $server - */ - private function getUri(array $server): UriInterface - { - $uri = $this->uriFactory->createUri(); - - if (array_key_exists('HTTPS', $server) && $server['HTTPS'] !== '' && $server['HTTPS'] !== 'off') { - $uri = $uri->withScheme('https'); - } else { - $uri = $uri->withScheme('http'); - } - - $uri = isset($server['SERVER_PORT']) - ? $uri->withPort((int)$server['SERVER_PORT']) - : $uri->withPort($uri->getScheme() === 'https' ? 443 : 80); - - if (isset($server['HTTP_HOST'])) { - $uri = preg_match('/^(.+):(\d+)$/', $server['HTTP_HOST'], $matches) === 1 - ? $uri - ->withHost($matches[1]) - ->withPort((int) $matches[2]) - : $uri->withHost($server['HTTP_HOST']) - ; - } elseif (isset($server['SERVER_NAME'])) { - $uri = $uri->withHost($server['SERVER_NAME']); - } - - if (isset($server['REQUEST_URI'])) { - $uri = $uri->withPath(explode('?', $server['REQUEST_URI'])[0]); - } - - if (isset($server['QUERY_STRING'])) { - $uri = $uri->withQuery($server['QUERY_STRING']); - } - - return $uri; - } - - /** - * @psalm-return array - */ - private function getHeadersFromGlobals(): array - { - if (function_exists('getallheaders') && ($headers = getallheaders()) !== false) { - /** @psalm-var array $headers */ - return $headers; - } - - $headers = []; - - /** - * @var string $name - * @var string $value - */ - foreach ($_SERVER as $name => $value) { - if (str_starts_with($name, 'REDIRECT_')) { - $name = substr($name, 9); - - if (array_key_exists($name, $_SERVER)) { - continue; - } - } - - if (str_starts_with($name, 'HTTP_')) { - $headers[$this->normalizeHeaderName(substr($name, 5))] = $value; - continue; - } - - if (str_starts_with($name, 'CONTENT_')) { - $headers[$this->normalizeHeaderName($name)] = $value; - } - } - - return $headers; - } - - private function normalizeHeaderName(string $name): string - { - return str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $name)))); - } - - private function getUploadedFilesArray(array $filesArray): array - { - $files = []; - - /** @var array $info */ - foreach ($filesArray as $class => $info) { - $files[$class] = []; - $this->populateUploadedFileRecursive( - $files[$class], - $info['name'], - $info['tmp_name'], - $info['type'], - $info['size'], - $info['error'], - ); - } - - return $files; - } - - /** - * Populates uploaded files array from $_FILE data structure recursively. - * - * @param array $files Uploaded files array to be populated. - * @param mixed $names File names provided by PHP. - * @param mixed $tempNames Temporary file names provided by PHP. - * @param mixed $types File types provided by PHP. - * @param mixed $sizes File sizes provided by PHP. - * @param mixed $errors Uploading issues provided by PHP. - * - * @psalm-suppress MixedArgument, ReferenceConstraintViolation - */ - private function populateUploadedFileRecursive( - array &$files, - mixed $names, - mixed $tempNames, - mixed $types, - mixed $sizes, - mixed $errors - ): void { - if (is_array($names)) { - /** @var array|string $name */ - foreach ($names as $i => $name) { - $files[$i] = []; - /** @psalm-suppress MixedArrayAccess */ - $this->populateUploadedFileRecursive( - $files[$i], - $name, - $tempNames[$i], - $types[$i], - $sizes[$i], - $errors[$i], - ); - } - - return; - } - - try { - $stream = $this->streamFactory->createStreamFromFile($tempNames); - } catch (RuntimeException) { - $stream = $this->streamFactory->createStream(); - } - - $files = $this->uploadedFileFactory->createUploadedFile( - $stream, - (int) $sizes, - (int) $errors, - $names, - $types - ); - } -} diff --git a/tests/ServerRequestFactoryTest.php b/tests/ServerRequestFactoryTest.php deleted file mode 100644 index a08b88b..0000000 --- a/tests/ServerRequestFactoryTest.php +++ /dev/null @@ -1,465 +0,0 @@ - 'test', - 'REQUEST_METHOD' => 'GET', - ]; - $_FILES = [ - 'file1' => [ - 'name' => $firstFileName = 'facepalm.jpg', - 'type' => 'image/jpeg', - 'tmp_name' => '/tmp/123', - 'error' => '0', - 'size' => '31059', - ], - 'file2' => [ - 'name' => [$secondFileName = 'facepalm2.jpg', $thirdFileName = 'facepalm3.jpg'], - 'type' => ['image/jpeg', 'image/jpeg'], - 'tmp_name' => ['/tmp/phpJutmOS', '/tmp/php9bNI8F'], - 'error' => ['0', '0'], - 'size' => ['78085', '61429'], - ], - ]; - - $serverRequest = $this - ->createServerRequestFactory() - ->createFromGlobals(); - - $firstUploadedFile = $serverRequest->getUploadedFiles()['file1']; - $this->assertSame($firstFileName, $firstUploadedFile->getClientFilename()); - - $secondUploadedFile = $serverRequest->getUploadedFiles()['file2'][0]; - $this->assertSame($secondFileName, $secondUploadedFile->getClientFilename()); - - $thirdUploadedFile = $serverRequest->getUploadedFiles()['file2'][1]; - $this->assertSame($thirdFileName, $thirdUploadedFile->getClientFilename()); - } - - public function testHeadersParsing(): void - { - $_SERVER = [ - 'HTTP_HOST' => 'example.com', - 'CONTENT_TYPE' => 'text/plain', - 'REQUEST_METHOD' => 'GET', - 'REDIRECT_STATUS' => '200', - 'REDIRECT_HTTP_HOST' => 'example.org', - 'REDIRECT_HTTP_CONNECTION' => 'keep-alive', - ]; - - $expected = [ - 'Host' => ['example.com'], - 'Content-Type' => ['text/plain'], - 'Connection' => ['keep-alive'], - ]; - - $serverRequest = $this - ->createServerRequestFactory() - ->createFromGlobals(); - - $this->assertSame($expected, $serverRequest->getHeaders()); - } - - public function testInvalidMethodException(): void - { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Unable to determine HTTP request method.'); - - $this - ->createServerRequestFactory() - ->createFromParameters([]); - } - - public static function bodyDataProvider(): array - { - $content = 'content'; - $resource = fopen('php://memory', 'wb+'); - fwrite($resource, $content); - rewind($resource); - - return [ - 'StreamFactoryInterface' => [(new StreamFactory())->createStream('content'), $content], - 'resource' => [$resource, $content], - 'string' => [$content, $content], - 'null' => [null, ''], - ]; - } - - /** - * @dataProvider bodyDataProvider - */ - public function testBody(mixed $body, string $expected): void - { - $server = ['REQUEST_METHOD' => 'GET']; - $request = $this - ->createServerRequestFactory() - ->createFromParameters($server, [], [], [], [], [], $body); - - $this->assertSame($expected, (string) $request->getBody()); - } - - public static function invalidBodyDataProvider(): array - { - return [ - 'int' => [1], - 'float' => [1.1], - 'true' => [true], - 'false' => [false], - 'empty-array' => [[]], - 'object' => [new StdClass()], - 'callable' => [static fn () => null], - ]; - } - - /** - * @dataProvider invalidBodyDataProvider - */ - public function testInvalidBodyException(mixed $body): void - { - $server = ['REQUEST_METHOD' => 'GET']; - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage( - 'Body parameter for "ServerRequestFactory::createFromParameters()"' - . 'must be instance of StreamInterface, resource or null.', - ); - - $this - ->createServerRequestFactory() - ->createFromParameters($server, [], [], [], [], [], $body); - } - - public static function hostParsingDataProvider(): array - { - return [ - 'host' => [ - [ - 'HTTP_HOST' => 'test', - 'REQUEST_METHOD' => 'GET', - ], - [ - 'method' => 'GET', - 'host' => 'test', - 'port' => null, - 'protocol' => '1.1', - 'scheme' => 'http', - 'path' => '', - 'query' => '', - ], - ], - 'hostWithPort' => [ - [ - 'HTTP_HOST' => 'test:88', - 'REQUEST_METHOD' => 'GET', - ], - [ - 'method' => 'GET', - 'host' => 'test', - 'port' => 88, - 'protocol' => '1.1', - 'scheme' => 'http', - 'path' => '', - 'query' => '', - ], - ], - 'ipv4' => [ - [ - 'HTTP_HOST' => '127.0.0.1', - 'REQUEST_METHOD' => 'GET', - 'HTTPS' => true, - ], - [ - 'method' => 'GET', - 'host' => '127.0.0.1', - 'port' => null, - 'protocol' => '1.1', - 'scheme' => 'https', - 'path' => '', - 'query' => '', - ], - ], - 'ipv4WithPort' => [ - [ - 'HTTP_HOST' => '127.0.0.1:443', - 'REQUEST_METHOD' => 'GET', - ], - [ - 'method' => 'GET', - 'host' => '127.0.0.1', - 'port' => 443, - 'protocol' => '1.1', - 'scheme' => 'http', - 'path' => '', - 'query' => '', - ], - ], - 'ipv6' => [ - [ - 'HTTP_HOST' => '[::1]', - 'REQUEST_METHOD' => 'GET', - ], - [ - 'method' => 'GET', - 'host' => '[::1]', - 'port' => null, - 'protocol' => '1.1', - 'scheme' => 'http', - 'path' => '', - 'query' => '', - ], - ], - 'ipv6WithPort' => [ - [ - 'HTTP_HOST' => '[::1]:443', - 'REQUEST_METHOD' => 'GET', - ], - [ - 'method' => 'GET', - 'host' => '[::1]', - 'port' => 443, - 'protocol' => '1.1', - 'scheme' => 'http', - 'path' => '', - 'query' => '', - ], - ], - 'serverName' => [ - [ - 'SERVER_NAME' => 'test', - 'REQUEST_METHOD' => 'GET', - ], - [ - 'method' => 'GET', - 'host' => 'test', - 'port' => null, - 'protocol' => '1.1', - 'scheme' => 'http', - 'path' => '', - 'query' => '', - ], - ], - 'hostAndServerName' => [ - [ - 'SERVER_NAME' => 'override', - 'HTTP_HOST' => 'test', - 'REQUEST_METHOD' => 'GET', - 'SERVER_PORT' => 81, - ], - [ - 'method' => 'GET', - 'host' => 'test', - 'port' => 81, - 'protocol' => '1.1', - 'scheme' => 'http', - 'path' => '', - 'query' => '', - ], - ], - 'none' => [ - [ - 'REQUEST_METHOD' => 'GET', - ], - [ - 'method' => 'GET', - 'host' => '', - 'port' => null, - 'protocol' => '1.1', - 'scheme' => 'http', - 'path' => '', - 'query' => '', - ], - ], - 'path' => [ - [ - 'REQUEST_METHOD' => 'GET', - 'REQUEST_URI' => '/path/to/folder?param=1', - ], - [ - 'method' => 'GET', - 'host' => '', - 'port' => null, - 'protocol' => '1.1', - 'scheme' => 'http', - 'path' => '/path/to/folder', - 'query' => '', - ], - ], - 'query' => [ - [ - 'REQUEST_METHOD' => 'GET', - 'QUERY_STRING' => 'path/to/folder?param=1', - ], - [ - 'method' => 'GET', - 'host' => '', - 'port' => null, - 'protocol' => '1.1', - 'scheme' => 'http', - 'path' => '', - 'query' => 'path/to/folder?param=1', - ], - ], - 'protocol' => [ - [ - 'REQUEST_METHOD' => 'GET', - 'SERVER_PROTOCOL' => 'HTTP/1.0', - ], - [ - 'method' => 'GET', - 'host' => '', - 'port' => null, - 'protocol' => '1.0', - 'scheme' => 'http', - 'path' => '', - 'query' => '', - ], - ], - 'post' => [ - [ - 'REQUEST_METHOD' => 'POST', - ], - [ - 'method' => 'POST', - 'host' => '', - 'port' => null, - 'protocol' => '1.1', - 'scheme' => 'http', - 'path' => '', - 'query' => '', - ], - ], - 'delete' => [ - [ - 'REQUEST_METHOD' => 'DELETE', - ], - [ - 'method' => 'DELETE', - 'host' => '', - 'port' => null, - 'protocol' => '1.1', - 'scheme' => 'http', - 'path' => '', - 'query' => '', - ], - ], - 'put' => [ - [ - 'REQUEST_METHOD' => 'PUT', - ], - [ - 'method' => 'PUT', - 'host' => '', - 'port' => null, - 'protocol' => '1.1', - 'scheme' => 'http', - 'path' => '', - 'query' => '', - ], - ], - 'https' => [ - [ - 'REQUEST_METHOD' => 'PUT', - 'HTTPS' => 'on', - ], - [ - 'method' => 'PUT', - 'host' => '', - 'port' => null, - 'protocol' => '1.1', - 'scheme' => 'https', - 'path' => '', - 'query' => '', - ], - ], - ]; - } - - /** - * @dataProvider hostParsingDataProvider - */ - public function testHostParsingFromParameters(array $serverParams, array $expectParams): void - { - $serverRequest = $this - ->createServerRequestFactory() - ->createFromParameters($serverParams); - - $this->assertSame($expectParams['host'], $serverRequest - ->getUri() - ->getHost()); - $this->assertSame($expectParams['port'], $serverRequest - ->getUri() - ->getPort()); - $this->assertSame($expectParams['method'], $serverRequest->getMethod()); - $this->assertSame($expectParams['protocol'], $serverRequest->getProtocolVersion()); - $this->assertSame($expectParams['scheme'], $serverRequest - ->getUri() - ->getScheme()); - $this->assertSame($expectParams['path'], $serverRequest - ->getUri() - ->getPath()); - $this->assertSame($expectParams['query'], $serverRequest - ->getUri() - ->getQuery()); - } - - /** - * @dataProvider hostParsingDataProvider - * @backupGlobals enabled - */ - public function testHostParsingFromGlobals(array $serverParams, array $expectParams): void - { - $_SERVER = $serverParams; - $serverRequest = $this - ->createServerRequestFactory() - ->createFromGlobals(); - - $this->assertSame($expectParams['host'], $serverRequest - ->getUri() - ->getHost()); - $this->assertSame($expectParams['port'], $serverRequest - ->getUri() - ->getPort()); - $this->assertSame($expectParams['method'], $serverRequest->getMethod()); - $this->assertSame($expectParams['protocol'], $serverRequest->getProtocolVersion()); - $this->assertSame($expectParams['scheme'], $serverRequest - ->getUri() - ->getScheme()); - $this->assertSame($expectParams['path'], $serverRequest - ->getUri() - ->getPath()); - $this->assertSame($expectParams['query'], $serverRequest - ->getUri() - ->getQuery()); - } - - private function createServerRequestFactory(): ServerRequestFactory - { - return new ServerRequestFactory( - new PsrServerRequestFactory(), - new UriFactory(), - new UploadedFileFactory(), - new StreamFactory(), - ); - } -}