├── LICENSE.md ├── src ├── ServerNormalizerInterface.php ├── ServerRequestCreator.php ├── PhpInputStream.php ├── SapiNormalizer.php └── UploadedFileCreator.php ├── composer.json ├── CHANGELOG.md └── README.md /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Evgeniy Zyubin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /src/ServerNormalizerInterface.php: -------------------------------------------------------------------------------- 1 | value` header pairs. 40 | */ 41 | public function normalizeHeaders(array $server): array; 42 | } 43 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "httpsoft/http-server-request", 3 | "description": "Infrastructure for creating PSR-7 ServerRequest and UploadedFile", 4 | "keywords": ["php", "http", "http-server-request", "psr-7", "http-message"], 5 | "homepage": "https://httpsoft.org/", 6 | "type": "library", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Evgeniy Zyubin", 11 | "email": "mail@devanych.ru", 12 | "homepage": "https://devanych.ru/", 13 | "role": "Founder and lead developer" 14 | } 15 | ], 16 | "support": { 17 | "issues": "https://github.com/httpsoft/http-server-request/issues", 18 | "source": "https://github.com/httpsoft/http-server-request", 19 | "docs": "https://httpsoft.org/docs/server-request" 20 | }, 21 | "require": { 22 | "php": "^7.4|^8.0", 23 | "httpsoft/http-message": "^1.1" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "^9.5", 27 | "squizlabs/php_codesniffer": "^3.7", 28 | "vimeo/psalm": "^4.9|^5.2" 29 | }, 30 | "provide": { 31 | "psr/http-message-implementation": "1.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "HttpSoft\\ServerRequest\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "HttpSoft\\Tests\\ServerRequest\\": "tests/" 41 | } 42 | }, 43 | "scripts": { 44 | "test": "phpunit --colors=always", 45 | "static": "psalm", 46 | "cs-check": "phpcs", 47 | "cs-fix": "phpcbf", 48 | "check": [ 49 | "@cs-check", 50 | "@static", 51 | "@test" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ServerRequestCreator.php: -------------------------------------------------------------------------------- 1 | normalizeMethod($server), 48 | $normalizer->normalizeUri($server), 49 | $normalizer->normalizeHeaders($server), 50 | new PhpInputStream(), 51 | $normalizer->normalizeProtocolVersion($server) 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/PhpInputStream.php: -------------------------------------------------------------------------------- 1 | init($stream, 'r'); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function __toString(): string 39 | { 40 | if (!$this->isEof) { 41 | $this->getContents(); 42 | } 43 | 44 | return $this->cache; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function isWritable(): bool 51 | { 52 | return false; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function read($length): string 59 | { 60 | $result = $this->readInternal($length); 61 | 62 | if (!$this->isEof) { 63 | $this->cache .= $result; 64 | } 65 | 66 | if ($this->eof()) { 67 | $this->isEof = true; 68 | } 69 | 70 | return $result; 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function getContents(): string 77 | { 78 | if ($this->isEof) { 79 | return $this->cache; 80 | } 81 | 82 | $result = $this->getContentsInternal(); 83 | $this->cache .= $result; 84 | $this->isEof = true; 85 | 86 | return $result; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # HTTP Server Request Change Log 2 | 3 | ## 1.1.1 - 2024.11.25 4 | 5 | ### Fixed 6 | 7 | - [#6](https://github.com/httpsoft/http-server-request/pull/6) Fixes deprecation notices for PHP 8.4. 8 | 9 | ## 1.1.0 - 2023.05.05 10 | 11 | ### Changed 12 | 13 | - [#5](https://github.com/httpsoft/http-server-request/pull/5) Rises `httpsoft/http-message` package version to `^1.1`. 14 | 15 | ## 1.0.6 - 2023.05.05 16 | 17 | ### Fixed 18 | 19 | - [#4](https://github.com/httpsoft/http-server-request/pull/4) Fixes parsing host from `HTTP_HOST` header to `HttpSoft\ServerRequest\SapiNormalizer`. 20 | 21 | ## 1.0.5 - 2021.07.20 22 | 23 | ### Added 24 | 25 | - Nothing. 26 | 27 | ### Changed 28 | 29 | - Nothing. 30 | 31 | ### Deprecated 32 | 33 | - Nothing. 34 | 35 | ### Removed 36 | 37 | - Nothing. 38 | 39 | ### Fixed 40 | 41 | - [Fixes](https://github.com/httpsoft/http-server-request/commit/6552246f34d767a33bb23b4348b8560d41e15136) `HttpSoft\ServerRequest\SapiNormalizer::normalizeHeaders()` method. 42 | - [#1](https://github.com/httpsoft/http-server-request/pull/1) adds test cases for code coverage and updates of workflow actions. 43 | 44 | ## 1.0.4 - 2020.12.12 45 | 46 | ### Added 47 | 48 | - Nothing. 49 | 50 | ### Changed 51 | 52 | - Updates development dependencies. 53 | - Updates GitHub actions. 54 | 55 | ### Deprecated 56 | 57 | - Nothing. 58 | 59 | ### Removed 60 | 61 | - Nothing. 62 | 63 | ### Fixed 64 | 65 | - Nothing. 66 | 67 | ## 1.0.3 - 2020.09.06 68 | 69 | ### Added 70 | 71 | - Adds implementations declaration to the `composer.json`. 72 | 73 | ### Changed 74 | 75 | - Nothing. 76 | 77 | ### Deprecated 78 | 79 | - Nothing. 80 | 81 | ### Removed 82 | 83 | - Nothing. 84 | 85 | ### Fixed 86 | 87 | - Nothing. 88 | 89 | ## 1.0.2 - 2020.08.28 90 | 91 | ### Added 92 | 93 | - Adds support OS Windows to build github action. 94 | - Adds files to `.github` folder (ISSUE_TEMPLATE, PULL_REQUEST_TEMPLATE.md, CODE_OF_CONDUCT.md, SECURITY.md). 95 | 96 | ### Changed 97 | 98 | - Moves static analysis and checking of the code standard to an independent github action. 99 | 100 | ### Deprecated 101 | 102 | - Nothing. 103 | 104 | ### Removed 105 | 106 | - Nothing. 107 | 108 | ### Fixed 109 | 110 | - Nothing. 111 | 112 | ## 1.0.1 - 2020.08.25 113 | 114 | ### Added 115 | 116 | - Nothing. 117 | 118 | ### Changed 119 | 120 | - Nothing. 121 | 122 | ### Deprecated 123 | 124 | - Nothing. 125 | 126 | ### Removed 127 | 128 | - Nothing. 129 | 130 | ### Fixed 131 | 132 | - Moves Psalm issue handlers from psalm.xml to docBlock to appropriate methods. 133 | 134 | ## 1.0.0 - 2020.08.23 135 | 136 | - Initial stable release. 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTP Server Request 2 | 3 | [![License](https://poser.pugx.org/httpsoft/http-server-request/license)](https://packagist.org/packages/httpsoft/http-server-request) 4 | [![Latest Stable Version](https://poser.pugx.org/httpsoft/http-server-request/v)](https://packagist.org/packages/httpsoft/http-server-request) 5 | [![Total Downloads](https://poser.pugx.org/httpsoft/http-server-request/downloads)](https://packagist.org/packages/httpsoft/http-server-request) 6 | [![GitHub Build Status](https://github.com/httpsoft/http-server-request/workflows/build/badge.svg)](https://github.com/httpsoft/http-server-request/actions) 7 | [![GitHub Static Analysis Status](https://github.com/httpsoft/http-server-request/workflows/static/badge.svg)](https://github.com/httpsoft/http-server-request/actions) 8 | [![Scrutinizer Code Coverage](https://scrutinizer-ci.com/g/httpsoft/http-server-request/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/httpsoft/http-server-request/?branch=master) 9 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/httpsoft/http-server-request/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/httpsoft/http-server-request/?branch=master) 10 | 11 | This package makes it easy and flexible to create PSR-7 components [ServerRequest](https://github.com/php-fig/http-message/blob/master/src/ServerRequestInterface.php) and [UploadedFile](https://github.com/php-fig/http-message/blob/master/src/UploadedFileInterface.php). 12 | 13 | Depends on the [httpsoft/http-message](https://github.com/httpsoft/http-message) package. 14 | 15 | ## Documentation 16 | 17 | * [In English language](https://httpsoft.org/docs/server-request). 18 | * [In Russian language](https://httpsoft.org/ru/docs/server-request). 19 | 20 | ## Installation 21 | 22 | This package requires PHP version 7.4 or later. 23 | 24 | ``` 25 | composer require httpsoft/http-server-request 26 | ``` 27 | 28 | ## Usage ServerRequestCreator 29 | 30 | ```php 31 | use HttpSoft\ServerRequest\ServerRequestCreator; 32 | 33 | // All necessary data will be received automatically: 34 | $request = ServerRequestCreator::createFromGlobals($_SERVER, $_FILES, $_COOKIE, $_GET, $_POST); 35 | // equivalently to: 36 | $request = ServerRequestCreator::createFromGlobals(); 37 | // equivalently to: 38 | $request = ServerRequestCreator::create(); 39 | ``` 40 | 41 | By default [HttpSoft\ServerRequest\SapiNormalizer](https://github.com/httpsoft/http-server-request/blob/master/src/SapiNormalizer.php) is used for normalization of server parameters. You can use your own server parameters normalizer, for this you need to implement an [HttpSoft\ServerRequest\ServerNormalizerInterface](https://github.com/httpsoft/http-server-request/blob/master/src/ServerNormalizerInterface.php) interface. 42 | 43 | ```php 44 | $normalizer = new YouCustomServerNormalizer(); 45 | 46 | $request = ServerRequestCreator::create($normalizer); 47 | // equivalently to: 48 | $request = ServerRequestCreator::createFromGlobals($_SERVER, $_FILES, $_COOKIE, $_GET, $_POST, $normalizer); 49 | // or with custom superglobals: 50 | $request = ServerRequestCreator::createFromGlobals($server, $files, $cookie, $get, $post, $normalizer); 51 | ``` 52 | 53 | ## Usage UploadedFileCreator 54 | 55 | ```php 56 | use HttpSoft\ServerRequest\UploadedFileCreator; 57 | 58 | /** @var StreamInterface|string|resource $streamOrFile */ 59 | $uploadedFile = UploadedFileCreator::create($streamOrFile, 1024, UPLOAD_ERR_OK, 'file.txt', 'text/plain'); 60 | 61 | // Create a new `HttpSoft\UploadedFile\UploadedFile` instance from array (the item `$_FILES`) 62 | $uploadedFile = UploadedFileCreator::createFromArray([ 63 | 'name' => 'filename.jpg', // optional 64 | 'type' => 'image/jpeg', // optional 65 | 'tmp_name' => '/tmp/php/php6hst32', 66 | 'error' => 0, // UPLOAD_ERR_OK 67 | 'size' => 98174, 68 | ]); 69 | 70 | // Normalizes the superglobal structure and converts each array 71 | // value to an instance of `Psr\Http\Message\UploadedFileInterface`. 72 | $uploadedFiles = UploadedFileCreator::createFromGlobals($_FILES); 73 | ``` 74 | -------------------------------------------------------------------------------- /src/SapiNormalizer.php: -------------------------------------------------------------------------------- 1 | withScheme('https'); 59 | } elseif ($scheme = $server['HTTP_X_FORWARDED_PROTO'] ?? $server['REQUEST_SCHEME'] ?? '') { 60 | $uri = $uri->withScheme((string) $scheme); 61 | } 62 | 63 | if (isset($server['SERVER_PORT'])) { 64 | $uri = $uri->withPort((int) $server['SERVER_PORT']); 65 | } 66 | 67 | if ($host = $server['HTTP_X_FORWARDED_HOST'] ?? $server['HTTP_HOST'] ?? '') { 68 | $uri = preg_match('/^(.+):(\d+)$/', (string) $host, $matches) === 1 69 | ? $uri->withHost($matches[1])->withPort((int) $matches[2]) 70 | : $uri->withHost((string) $host); 71 | } elseif ($host = $server['SERVER_NAME'] ?? $server['SERVER_ADDR'] ?? '') { 72 | $uri = $uri->withHost((string) $host); 73 | } 74 | 75 | if ($path = $server['REQUEST_URI'] ?? $server['ORIG_PATH_INFO'] ?? '') { 76 | $uri = $uri->withPath(explode('?', preg_replace('/^[^\/:]+:\/\/[^\/]+/', '', (string) $path), 2)[0]); 77 | } 78 | 79 | if (isset($server['QUERY_STRING'])) { 80 | $uri = $uri->withQuery((string) $server['QUERY_STRING']); 81 | } 82 | 83 | return $uri; 84 | } 85 | 86 | /** 87 | * {@inheritDoc} 88 | * 89 | * @psalm-suppress MixedAssignment 90 | */ 91 | public function normalizeHeaders(array $server): array 92 | { 93 | $headers = []; 94 | 95 | foreach ($server as $name => $value) { 96 | if (!is_string($name)) { 97 | continue; 98 | } 99 | 100 | if (strpos($name, 'REDIRECT_') === 0) { 101 | if (array_key_exists($name = substr($name, 9), $server)) { 102 | continue; 103 | } 104 | } 105 | 106 | if (strpos($name, 'HTTP_') === 0) { 107 | $headers[$this->normalizeHeaderName(substr($name, 5))] = $value; 108 | continue; 109 | } 110 | 111 | if (strpos($name, 'CONTENT_') === 0) { 112 | $headers[$this->normalizeHeaderName($name)] = $value; 113 | } 114 | } 115 | 116 | return $headers; 117 | } 118 | 119 | /** 120 | * @param string $name 121 | * @return string 122 | */ 123 | private function normalizeHeaderName(string $name): string 124 | { 125 | return str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $name)))); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/UploadedFileCreator.php: -------------------------------------------------------------------------------- 1 | 'filename.jpg', // optional 46 | * 'type' => 'image/jpeg', // optional 47 | * 'tmp_name' => '/tmp/php/php6hst32', 48 | * 'error' => 0, // UPLOAD_ERR_OK 49 | * 'size' => 98174, 50 | * ]; 51 | * ``` 52 | * 53 | * @see https://www.php.net/manual/features.file-upload.post-method.php 54 | * @see https://www.php.net/manual/reserved.variables.files.php 55 | * 56 | * @param array $file 57 | * @return UploadedFileInterface 58 | * @throws InvalidArgumentException 59 | * @psalm-suppress MixedArgument 60 | */ 61 | public static function createFromArray(array $file): UploadedFileInterface 62 | { 63 | if (!isset($file['tmp_name']) || !isset($file['size']) || !isset($file['error'])) { 64 | throw new InvalidArgumentException(sprintf( 65 | 'Invalid array `$file` to `%s`. One of the items is missing: "tmp_name" or "size" or "error".', 66 | __METHOD__ 67 | )); 68 | } 69 | 70 | return new UploadedFile( 71 | $file['tmp_name'], 72 | $file['size'], 73 | $file['error'], 74 | $file['name'] ?? null, 75 | $file['type'] ?? null 76 | ); 77 | } 78 | 79 | /** 80 | * Converts each value of the multidimensional array `$files` 81 | * to an `Psr\Http\Message\UploadedFileInterface` instance. 82 | * 83 | * The method uses recursion, so the `$files` array can be of any nesting type. 84 | * The array structure must be the same as the global `$_FILES` array. 85 | * All key names in the `$files` array will be saved. 86 | * 87 | * @see https://www.php.net/manual/features.file-upload.post-method.php 88 | * @see https://www.php.net/manual/reserved.variables.files.php 89 | * 90 | * @param array $files 91 | * @return UploadedFileInterface[]|array[] 92 | * @throws InvalidArgumentException 93 | * @psalm-suppress MixedAssignment 94 | */ 95 | public static function createFromGlobals(array $files = []): array 96 | { 97 | $uploadedFiles = []; 98 | 99 | foreach ($files as $key => $file) { 100 | if ($file instanceof UploadedFileInterface) { 101 | $uploadedFiles[$key] = $file; 102 | continue; 103 | } 104 | 105 | if (!is_array($file)) { 106 | throw new InvalidArgumentException(sprintf( 107 | 'Error in the `%s`. Invalid file specification for normalize in array `$files`.', 108 | __METHOD__ 109 | )); 110 | } 111 | 112 | if (!isset($file['tmp_name'])) { 113 | $uploadedFiles[$key] = self::createFromGlobals($file); 114 | continue; 115 | } 116 | 117 | if (is_array($file['tmp_name'])) { 118 | $uploadedFiles[$key] = self::createMultipleUploadedFiles($file); 119 | continue; 120 | } 121 | 122 | $uploadedFiles[$key] = self::createFromArray($file); 123 | } 124 | 125 | return $uploadedFiles; 126 | } 127 | 128 | /** 129 | * Creates an array instances of `Psr\Http\Message\UploadedFileInterface` from an multidimensional array `$files`. 130 | * The array structure must be the same as multidimensional item in the global `$_FILES` array. 131 | * 132 | * @param array $files 133 | * @return UploadedFileInterface[] 134 | * @throws InvalidArgumentException 135 | * @psalm-suppress MixedArgument 136 | * @psalm-suppress MixedArgumentTypeCoercion 137 | */ 138 | private static function createMultipleUploadedFiles(array $files): array 139 | { 140 | if ( 141 | !isset($files['tmp_name']) || !is_array($files['tmp_name']) 142 | || !isset($files['size']) || !is_array($files['size']) 143 | || !isset($files['error']) || !is_array($files['error']) 144 | ) { 145 | throw new InvalidArgumentException(sprintf( 146 | 'Invalid array `$files` to `%s`. One of the items is missing or is not an array:' 147 | . ' "tmp_name" or "size" or "error".', 148 | __METHOD__ 149 | )); 150 | } 151 | 152 | return self::buildTree( 153 | $files['tmp_name'], 154 | $files['size'], 155 | $files['error'], 156 | $files['name'] ?? null, 157 | $files['type'] ?? null, 158 | ); 159 | } 160 | 161 | /** 162 | * Building a normalized tree with the correct nested structure 163 | * and `Psr\Http\Message\UploadedFileInterface` instances. 164 | * 165 | * @param string[]|array[] $tmpNames 166 | * @param int[]|array[] $sizes 167 | * @param int[]|array[] $errors 168 | * @param string[]|array[]|null $names 169 | * @param string[]|array[]|null $types 170 | * @return UploadedFileInterface[] 171 | * @psalm-suppress InvalidReturnType 172 | * @psalm-suppress InvalidReturnStatement 173 | * @psalm-suppress MixedArgumentTypeCoercion 174 | */ 175 | private static function buildTree(array $tmpNames, array $sizes, array $errors, ?array $names, ?array $types): array 176 | { 177 | $tree = []; 178 | 179 | foreach ($tmpNames as $key => $value) { 180 | if (is_array($value)) { 181 | $tree[$key] = self::buildTree( 182 | $tmpNames[$key], 183 | $sizes[$key], 184 | $errors[$key], 185 | $names[$key] ?? null, 186 | $types[$key] ?? null, 187 | ); 188 | } else { 189 | $tree[$key] = self::createFromArray([ 190 | 'tmp_name' => $tmpNames[$key], 191 | 'size' => $sizes[$key], 192 | 'error' => $errors[$key], 193 | 'name' => $names[$key] ?? null, 194 | 'type' => $types[$key] ?? null, 195 | ]); 196 | } 197 | } 198 | 199 | return $tree; 200 | } 201 | } 202 | --------------------------------------------------------------------------------