├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── run_all_tests.sh ├── src ├── Application.php ├── Common │ └── Environment.php ├── ContainerProvider.php ├── Controller │ └── BaseController.php ├── Di │ ├── Container.php │ └── ElementDefinition.php ├── Dispatcher │ └── Dispatcher.php ├── Exceptions │ ├── ContainerException.php │ ├── DispatcherException.php │ ├── RouteException.php │ └── RouterException.php ├── Http │ ├── Body.php │ ├── Cookies.php │ ├── Headers.php │ ├── Message.php │ ├── Request.php │ ├── RequestBody.php │ ├── Response.php │ ├── Stream.php │ ├── UploadedFile.php │ └── Uri.php ├── Interfaces │ ├── ContainerInterface.php │ ├── ContainerProviderInterface.php │ ├── DispatcherInterface.php │ ├── Http │ │ ├── CookiesInterface.php │ │ └── HeadersInterface.php │ ├── MapInterface.php │ └── RouterInterface.php ├── Router │ ├── Route.php │ └── Router.php └── Util │ └── Map.php └── tests ├── Test ├── ApplicationTest.php ├── DI │ └── ContainerTest.php └── Router │ ├── RouteTest.php │ └── RouterTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### SublimeText template 3 | # cache files for sublime text 4 | *.tmlanguage.cache 5 | *.tmPreferences.cache 6 | *.stTheme.cache 7 | 8 | # workspace files are user-specific 9 | *.sublime-workspace 10 | 11 | # project files should be checked into the repository, unless a significant 12 | # proportion of contributors will probably not be using SublimeText 13 | # *.sublime-project 14 | 15 | # sftp configuration file 16 | sftp-config.json 17 | ### Vagrant template 18 | .vagrant/ 19 | ### Node template 20 | # Logs 21 | logs 22 | *.log 23 | npm-debug.log* 24 | 25 | # Runtime data 26 | pids 27 | *.pid 28 | *.seed 29 | 30 | # Directory for instrumented libs generated by jscoverage/JSCover 31 | lib-cov 32 | 33 | # Coverage directory used by tools like istanbul 34 | coverage 35 | 36 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (http://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directory 46 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 47 | node_modules 48 | ### OSX template 49 | .DS_Store 50 | .AppleDouble 51 | .LSOverride 52 | 53 | # Icon must end with two \r 54 | Icon 55 | 56 | # Thumbnails 57 | ._* 58 | 59 | # Files that might appear in the root of a volume 60 | .DocumentRevisions-V100 61 | .fseventsd 62 | .Spotlight-V100 63 | .TemporaryItems 64 | .Trashes 65 | .VolumeIcon.icns 66 | 67 | # Directories potentially created on remote AFP share 68 | .AppleDB 69 | .AppleDesktop 70 | Network Trash Folder 71 | Temporary Items 72 | .apdisk 73 | ### Vim template 74 | [._]*.s[a-w][a-z] 75 | [._]s[a-w][a-z] 76 | *.un~ 77 | Session.vim 78 | .netrwhist 79 | *~ 80 | ### Eclipse template 81 | *.pydevproject 82 | .metadata 83 | .gradle 84 | tmp/ 85 | *.tmp 86 | *.bak 87 | *.swp 88 | *~.nib 89 | local.properties 90 | .settings/ 91 | .loadpath 92 | 93 | # Eclipse Core 94 | .project 95 | 96 | # External tool builders 97 | .externalToolBuilders/ 98 | 99 | # Locally stored "Eclipse launch configurations" 100 | *.launch 101 | 102 | # CDT-specific 103 | .cproject 104 | 105 | # JDT-specific (Eclipse Java Development Tools) 106 | .classpath 107 | 108 | # Java annotation processor (APT) 109 | .factorypath 110 | 111 | # PDT-specific 112 | .buildpath 113 | 114 | # sbteclipse plugin 115 | .target 116 | 117 | # TeXlipse plugin 118 | .texlipse 119 | ### JetBrains template 120 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 121 | 122 | *.iml 123 | 124 | ## Directory-based project format: 125 | .idea/ 126 | # if you remove the above rule, at least ignore the following: 127 | 128 | # User-specific stuff: 129 | # .idea/workspace.xml 130 | # .idea/tasks.xml 131 | # .idea/dictionaries 132 | 133 | # Sensitive or high-churn files: 134 | # .idea/dataSources.ids 135 | # .idea/dataSources.xml 136 | # .idea/sqlDataSources.xml 137 | # .idea/dynamic.xml 138 | # .idea/uiDesigner.xml 139 | 140 | # Gradle: 141 | # .idea/gradle.xml 142 | # .idea/libraries 143 | 144 | # Mongo Explorer plugin: 145 | # .idea/mongoSettings.xml 146 | 147 | ## File-based project format: 148 | *.ipr 149 | *.iws 150 | 151 | ## Plugin-specific files: 152 | 153 | # IntelliJ 154 | /out/ 155 | 156 | # mpeltonen/sbt-idea plugin 157 | .idea_modules/ 158 | 159 | # JIRA plugin 160 | atlassian-ide-plugin.xml 161 | 162 | # Crashlytics plugin (for Android Studio and IntelliJ) 163 | com_crashlytics_export_strings.xml 164 | crashlytics.properties 165 | crashlytics-build.properties 166 | 167 | ## Project format: 168 | vendor/ 169 | composer.phar 170 | composer.lock 171 | phpunit.phar 172 | .env -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2017 Mac Chow 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smile Framework 2 | 3 | 这是配合 [SegmentFault](https://segmentfault.com/l/1500000008993916) 上的视频讲堂专门编写的框架 4 | 5 | ## 安装 6 | 7 | 8 | 9 | ## 许可证 10 | 11 | Smile Framework 采用 MIT 许可证 授权. 查看 [MIT 许可证](LICENSE.md) 获取更多信息. 12 | 13 | 部分代码来自 Slim Framework, 版权信息: 14 | 15 | ``` 16 | Copyright (c) 2011-2016 Josh Lockhart 17 | 18 | https://github.com/slimphp/Slim/blob/3.x/LICENSE.md (MIT License) 19 | ``` 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vimac/smile-framework", 3 | "type": "library", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Mac Chow", 8 | "email": "vifix.mac@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "psr/http-message": "^1.0" 13 | }, 14 | "autoload": { 15 | "psr-4": { 16 | "Smile\\" : "src" 17 | } 18 | }, 19 | "autoload-dev": { 20 | "psr-4": { 21 | "Smile\\" : "tests" 22 | } 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "^5.7" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /run_all_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd $(dirname $0) 4 | if [ -f ./vendor/phpunit/phpunit/phpunit ]; then 5 | ./vendor/phpunit/phpunit/phpunit --bootstrap ./tests/bootstrap.php ./tests 6 | else 7 | echo "phpunit not installed by composer, check composer.json" 8 | fi 9 | -------------------------------------------------------------------------------- /src/Application.php: -------------------------------------------------------------------------------- 1 | set( 35 | (new ElementDefinition()) 36 | ->setType(ContainerInterface::class) 37 | ->setInstance($container) 38 | ->setAlias('container') 39 | ); 40 | 41 | //初始化ContainerProvider 42 | /** @var ContainerProvider $providerInstance */ 43 | $providerInstance = new $provider; 44 | $container->set( 45 | (new ElementDefinition()) 46 | ->setType($provider) 47 | ->setInstance($providerInstance) 48 | ); 49 | $providerInstance->setupContainer($container); 50 | 51 | if (isset($containerLoader)) { 52 | call_user_func($containerLoader, $container); 53 | } 54 | 55 | $this->container = $container; 56 | } 57 | 58 | /** 59 | * @return ContainerInterface 60 | */ 61 | public function getContainer() 62 | { 63 | return $this->container; 64 | } 65 | 66 | public function loadRouterConfig(callable $routerLoader = null) 67 | { 68 | /** @var Router $router */ 69 | $router = $this->container->getByAlias('router'); 70 | 71 | if (isset($routerLoader)) { 72 | call_user_func($routerLoader, $router); 73 | } 74 | } 75 | 76 | public function run() 77 | { 78 | /** @var ServerRequestInterface $request */ 79 | $request = $this->container->getByAlias('request'); 80 | 81 | /** @var ResponseInterface $response */ 82 | $response = $this->container->getByAlias('response'); 83 | 84 | /** @var RouterInterface $router */ 85 | $router = $this->container->getByAlias('router'); 86 | 87 | $route = $router->resolve($request->getMethod(), $request->getUri()->getPath()); 88 | 89 | /** @var DispatcherInterface $dispatcher */ 90 | $dispatcher = $this->container->getByAlias('dispatcher'); 91 | 92 | $callable = $dispatcher->dispatch($route, $request); 93 | 94 | $newResponse = call_user_func($callable, $request, $response); 95 | 96 | $this->respond($newResponse); 97 | } 98 | 99 | /** 100 | * 把Response返回到客户端 101 | * 102 | * @param ResponseInterface $response 103 | */ 104 | public function respond(ResponseInterface $response) 105 | { 106 | // Send response 107 | if (!headers_sent()) { 108 | // Status 109 | header(sprintf( 110 | 'HTTP/%s %s %s', 111 | $response->getProtocolVersion(), 112 | $response->getStatusCode(), 113 | $response->getReasonPhrase() 114 | )); 115 | 116 | // Headers 117 | foreach ($response->getHeaders() as $name => $values) { 118 | foreach ($values as $value) { 119 | header(sprintf('%s: %s', $name, $value), false); 120 | } 121 | } 122 | } 123 | 124 | // Body 125 | if (!$this->isEmptyResponse($response)) { 126 | $body = $response->getBody(); 127 | if ($body->isSeekable()) { 128 | $body->rewind(); 129 | } 130 | $chunkSize = 1024; //暂时写死 131 | 132 | $contentLength = $response->getHeaderLine('Content-Length'); 133 | if (!$contentLength) { 134 | $contentLength = $body->getSize(); 135 | } 136 | 137 | 138 | if (isset($contentLength)) { 139 | $amountToRead = $contentLength; 140 | while ($amountToRead > 0 && !$body->eof()) { 141 | $data = $body->read(min($chunkSize, $amountToRead)); 142 | echo $data; 143 | 144 | $amountToRead -= strlen($data); 145 | 146 | if (connection_status() != CONNECTION_NORMAL) { 147 | break; 148 | } 149 | } 150 | } else { 151 | while (!$body->eof()) { 152 | echo $body->read($chunkSize); 153 | if (connection_status() != CONNECTION_NORMAL) { 154 | break; 155 | } 156 | } 157 | } 158 | } 159 | } 160 | 161 | /** 162 | * 获取返回 Response 是否为空 163 | * 164 | * @param ResponseInterface $response 165 | * @return bool 166 | */ 167 | protected function isEmptyResponse(ResponseInterface $response) 168 | { 169 | if (method_exists($response, 'isEmpty')) { 170 | return $response->isEmpty(); 171 | } 172 | 173 | return in_array($response->getStatusCode(), [204, 205, 304]); 174 | } 175 | } -------------------------------------------------------------------------------- /src/Common/Environment.php: -------------------------------------------------------------------------------- 1 | set( 37 | (new ElementDefinition()) 38 | ->setType(Environment::class) 39 | ->setBuilder(function () { 40 | return new Environment($_SERVER); 41 | }) 42 | ->setSingletonScope() 43 | ->setAlias('environment') 44 | ); 45 | $container->set( 46 | (new ElementDefinition()) 47 | ->setType(ServerRequestInterface::class) 48 | ->setBuilder(function (Environment $environment) { 49 | return Request::createFromEnvironment($environment); 50 | }) 51 | ->setSingletonScope() 52 | ->setAlias('request') 53 | ); 54 | $container->set( 55 | (new ElementDefinition()) 56 | ->setType(ResponseInterface::class) 57 | ->setBuilder(function () { 58 | $headers = new Headers(['Content-Type' => 'text/html; charset=UTF-8']); 59 | $response = new Response(200, $headers); 60 | return $response; 61 | }) 62 | ->setSingletonScope() 63 | ->setAlias('response') 64 | ); 65 | $container->set( 66 | (new ElementDefinition()) 67 | ->setType(RouterInterface::class) 68 | ->setBuilder(function () { 69 | return new Router(); 70 | }) 71 | ->setSingletonScope() 72 | ->setAlias('router') 73 | ); 74 | $container->set( 75 | (new ElementDefinition()) 76 | ->setType(DispatcherInterface::class) 77 | ->setBuilder(function (ContainerInterface $container) { 78 | return new Dispatcher($container); 79 | }) 80 | ->setSingletonScope() 81 | ->setAlias('dispatcher') 82 | ); 83 | } 84 | 85 | } -------------------------------------------------------------------------------- /src/Controller/BaseController.php: -------------------------------------------------------------------------------- 1 | container = $container; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/Di/Container.php: -------------------------------------------------------------------------------- 1 | assertTypeNameAvailable($definition->getType()); 56 | 57 | //断言作用域 58 | $this->assertScope($definition); 59 | 60 | //如果没有设置实例则尝试初始化builder 61 | if ($definition->isInstanceNull()) { 62 | $this->initializeBuilder($definition); 63 | } 64 | 65 | if (!$definition->isBaseType()) { 66 | //保存到map中 67 | $this->definitionTypeMap[$definition->getType()] = $definition; 68 | } else { 69 | //如果是基本类型, 校验基本类型的合法性 70 | $this->assertBaseType($definition); 71 | } 72 | 73 | $alias = $definition->getAlias(); 74 | if (!empty($alias)) { 75 | $this->assertAliasAvailable($alias); 76 | $this->definitionAliasMap[$definition->getAlias()] = $definition; 77 | } 78 | 79 | $this->initializeEagerDefinition($definition); 80 | } 81 | 82 | /** 83 | * 根据类型从容器获得一个元素产生的实例 84 | * 85 | * @param $type 86 | * @return mixed 87 | */ 88 | public function getByType($type) 89 | { 90 | return $this->buildByTypeRecursive($type); 91 | } 92 | 93 | /** 94 | * 从容器获得一个元素产生的实例 95 | * 96 | * @param string $alias 元素别名 97 | * @return mixed 98 | * @throws ContainerException 99 | */ 100 | public function getByAlias($alias) 101 | { 102 | return $this->buildByAliasRecursive($alias); 103 | } 104 | 105 | /** 106 | * 给某个命名空间开启自动组装 107 | * 从使用效果来讲, 期望等价于依次给某个命名空间下的类调用: 108 | * $container->set( 109 | * (new ElementDefinition()) 110 | * ->setType(MyClass::class) 111 | * ->setDeferred() 112 | * ->setPrototypeScope() 113 | * ); 114 | * 不过一直到类名被getByType访问之前, 都不会被调用 115 | * 116 | * @fixme 严格的讲, 这个不应该属于容器的职责, 大家可以考虑一下如何把这部分逻辑剥离出容器的接口 117 | * 118 | * @param string $namespace 119 | * @return mixed 120 | * @throws ContainerException 121 | */ 122 | public function enableAutowiredForNamespace($namespace) 123 | { 124 | $this->assertNamespaceAvailable($namespace); 125 | $this->autowiredNamespaces[] = $namespace; 126 | } 127 | 128 | /** 129 | * 递归的创建实例 130 | * 第二个参数主要用于检查循环依赖 131 | * 132 | * @param string $type 类型名 133 | * @param array $stack 依赖栈 134 | * @return mixed 135 | * @throws ContainerException 136 | */ 137 | private function buildByTypeRecursive($type, array $stack = []) 138 | { 139 | $definition = null; 140 | $this->assertNoCircleDependency($type, $stack); 141 | $stack[] = $type; 142 | 143 | if (isset($this->definitionTypeMap[$type])) { 144 | $definition = $this->definitionTypeMap[$type]; 145 | } elseif ($this->searchAutowiredNamespace($type)) { 146 | $autoDefinition = (new ElementDefinition()) 147 | ->setType($type) 148 | ->setBuilderToConstructor() 149 | ->setPrototypeScope() 150 | ->setDeferred(); 151 | $this->set($autoDefinition); 152 | $definition = $autoDefinition; 153 | } else { 154 | throw new ContainerException(sprintf('找不到定义: %s, 依赖栈: %s', $type, json_encode($stack))); 155 | } 156 | 157 | if ($definition->isSingletonScope() and !$definition->isInstanceNull()) { 158 | // 单例并且已经初始化的实例直接返回 159 | $result = $definition->getInstance(); 160 | $this->assertResultType($definition, $result); 161 | return $result; 162 | } 163 | $result = $this->callBuilder($definition, $stack); 164 | $this->assertResultType($definition, $result); 165 | if ($definition->isSingletonScope()) { 166 | // 如果是单例, 保存这个实例 167 | $definition->setInstance($result); 168 | } 169 | return $result; 170 | } 171 | 172 | /** 173 | * 递归的创建实例 174 | * 第二个参数主要用于检查循环依赖 175 | * 176 | * @param string $alias 别名 177 | * @param array $stack 依赖栈 178 | * @return mixed 179 | * @throws ContainerException 180 | */ 181 | private function buildByAliasRecursive($alias, array $stack = []) 182 | { 183 | $definition = null; 184 | $this->assertNoCircleDependency('$' . $alias, $stack); 185 | $stack[] = '$' . $alias; 186 | 187 | if (isset($this->definitionAliasMap[$alias])) { 188 | $definition = $this->definitionAliasMap[$alias]; 189 | } else { 190 | throw new ContainerException(sprintf('找不到别名: %s, 依赖栈: %s', $alias, json_encode($stack))); 191 | } 192 | 193 | if ($definition->isSingletonScope() and !$definition->isInstanceNull()) { 194 | // 单例并且已经初始化的实例直接返回 195 | $result = $definition->getInstance(); 196 | $this->assertResultType($definition, $result); 197 | return $result; 198 | } 199 | $result = $this->callBuilder($definition, $stack); 200 | $this->assertResultType($definition, $result); 201 | if ($definition->isSingletonScope()) { 202 | // 如果是单例, 保存这个实例 203 | $definition->setInstance($result); 204 | } 205 | return $result; 206 | } 207 | 208 | /** 209 | * 搜索是否命中自动组装的命名空间 210 | * 211 | * @param string $name 类名 212 | * @return bool 213 | */ 214 | private function searchAutowiredNamespace($name) 215 | { 216 | foreach ($this->autowiredNamespaces as $ns) { 217 | if (substr_compare($name, $ns, 0, strlen($ns)) === 0) { 218 | return true; 219 | } 220 | } 221 | return false; 222 | } 223 | 224 | /** 225 | * 断言某个类型是否是有效的类型, 如果无效, 则抛出异常 226 | * (但不发起类是否存在的验证) 227 | * 228 | * @param string $type 待检查的类型 229 | * @throws ContainerException 230 | */ 231 | private function assertTypeNameAvailable($type) 232 | { 233 | if (!is_string($type) or empty($type)) { 234 | throw new ContainerException('不是一个合法的类型'); 235 | } 236 | if (array_key_exists($type, $this->definitionTypeMap)) { 237 | throw new ContainerException('类型已经存在定义'); 238 | } 239 | } 240 | 241 | 242 | /** 243 | * 断言某个别名是一个有效的字符串, 如果无效, 则抛出异常 244 | * 一个别名只允许被设置一次 245 | * 246 | * @param string $name 待检查的别名 247 | * @throws ContainerException 248 | */ 249 | private function assertAliasAvailable($name) 250 | { 251 | if (!is_string($name)) { 252 | throw new ContainerException('不是一个合法的元素别名'); 253 | } 254 | if (array_key_exists($name, $this->definitionAliasMap)) { 255 | throw new ContainerException('别名已经存在'); 256 | } 257 | } 258 | 259 | /** 260 | * 校验基本类型是否合法 261 | * 262 | * @param ElementDefinition $definition 263 | * @throws ContainerException 264 | */ 265 | private function assertBaseType(ElementDefinition $definition) 266 | { 267 | if (empty($definition->getAlias())) { 268 | throw new ContainerException('基本类型的元素定义必须设置别名'); 269 | } 270 | } 271 | 272 | 273 | /** 274 | * 断言某个命名空间有效 275 | * 276 | * @param string $namespace 命名空间 277 | * @throws ContainerException 278 | */ 279 | private function assertNamespaceAvailable($namespace) 280 | { 281 | if (!is_string($namespace) or empty($namespace)) { 282 | throw new ContainerException('不是一个合法的命名空间'); 283 | } 284 | } 285 | 286 | /** 287 | * 断言元素定义下面的作用域设置 288 | * 289 | * @param ElementDefinition $definition 待检查的元素定义 290 | * @throws ContainerException 291 | */ 292 | private function assertScope(ElementDefinition $definition) 293 | { 294 | if ($definition->isSingletonScope()) { 295 | return; 296 | } elseif ($definition->isPrototypeScope()) { 297 | if ($definition->isEager()) { 298 | //不支持原型作用域的立即实例化 299 | throw new ContainerException('原型作用域不支持立即实例化'); 300 | } 301 | if (!$definition->isInstanceNull()) { 302 | throw new ContainerException('原型作用域不支持直接设置实例(必须提供builder创建)'); 303 | } 304 | } else { 305 | throw new ContainerException('不明作用域'); 306 | } 307 | } 308 | 309 | private function assertBuilderAvailable(ElementDefinition $definition) 310 | { 311 | if (!$definition->isBuilderEqualsConstructor() and !is_callable($definition->getBuilder())) { 312 | throw new ContainerException('builder不是一个合法的回调方法'); 313 | } 314 | } 315 | 316 | /** 317 | * 校验没有循环依赖 318 | * 319 | * @param $key 320 | * @param $stack 321 | * @throws ContainerException 322 | */ 323 | private function assertNoCircleDependency($key, $stack) 324 | { 325 | if (in_array($key, $stack)) { 326 | throw new ContainerException(sprintf('存在循环依赖, 依赖栈: %s', json_encode($stack))); 327 | } 328 | } 329 | 330 | /** 331 | * 校验返回值是否和定义的一致 332 | * 333 | * @param ElementDefinition $definition 334 | * @param mixed $buildResult 335 | * @throws ContainerException 336 | * @internal param $result 337 | */ 338 | private function assertResultType(ElementDefinition $definition, $buildResult) 339 | { 340 | if ($definition->isBaseType()) { 341 | if (!call_user_func('is_' . $definition->getType(), $buildResult)) { 342 | foreach (ElementDefinition::BASE_TYPES as $baseType) { 343 | if (call_user_func('is_' . $baseType, $buildResult)) { 344 | throw new ContainerException(sprintf('期望返回值类型: %s, 实际返回值类型: %s', $definition->getType(), $baseType)); 345 | } 346 | } 347 | } 348 | } else { 349 | if (!is_a($buildResult, $definition->getType())) { 350 | throw new ContainerException(sprintf('期望返回值类型: %s, 实际返回值类型: %s', $definition->getType(), get_class($buildResult))); 351 | } 352 | } 353 | 354 | } 355 | 356 | /** 357 | * 实例化立即初始化的元素定义 358 | * 359 | * @param ElementDefinition $definition 360 | * @return mixed 361 | */ 362 | private function initializeEagerDefinition(ElementDefinition $definition) 363 | { 364 | // 只初始化需要立即初始化的元素定义 365 | if ($definition->isEager()) { 366 | 367 | if (!$definition->isInstanceNull()) { 368 | //如果已经存在instance, 则跳过 369 | return $definition->getInstance(); 370 | } 371 | 372 | $type = $definition->getType(); 373 | 374 | if (class_exists($type, true)) { 375 | $definition->setInstance( 376 | $this->getByType($definition->getType()) 377 | ); 378 | } 379 | } 380 | } 381 | 382 | /** 383 | * 初始化&验证构造回调 384 | * 385 | * @param ElementDefinition $definition 386 | * @throws ContainerException 387 | */ 388 | private function initializeBuilder(ElementDefinition $definition) 389 | { 390 | if (empty($definition->getBuilder())) { 391 | if ($definition->isBaseType() and $definition->isInstanceNull()) { 392 | throw new ContainerException('基本类型不支持构造方法'); 393 | } 394 | //如果builder不存在, 设置类的构造方法为builder 395 | $definition->setBuilderToConstructor(); 396 | } else { 397 | //如果设置了builder, 则验证是否是callable的对象 398 | $this->assertBuilderAvailable($definition); 399 | } 400 | } 401 | 402 | /** 403 | * 调用Builder, 实例化方法 404 | * @param ElementDefinition $definition 405 | * @param array $stack 406 | * @return mixed 407 | * @throws ContainerException 408 | */ 409 | private function callBuilder(ElementDefinition $definition, array $stack = []) 410 | { 411 | $reflectionClass = null; 412 | $reflectionFunc = null; 413 | if ($definition->isBuilderEqualsConstructor()) { 414 | //判断builder方法定义成了目标类的构造方法 415 | $reflectionClass = new ReflectionClass($definition->getType()); 416 | $reflectionFunc = $reflectionClass->getConstructor(); 417 | if (empty($reflectionFunc)) { 418 | //目标对象不存在构造方法, 则直接生成一个对象的实例返回 419 | return $reflectionClass->newInstance(); 420 | } 421 | if (!$reflectionFunc->isPublic()) { 422 | throw new ContainerException(sprintf('构造方法作用域不可见, 依赖栈: %s', json_encode($stack))); 423 | } 424 | } else { 425 | $reflectionFunc = new ReflectionFunction($definition->getBuilder()); 426 | } 427 | 428 | $reflectionParams = $reflectionFunc->getParameters(); 429 | 430 | $realParams = []; 431 | foreach ($reflectionParams as $reflectionParam) { 432 | $reflectionParamClass = $reflectionParam->getClass(); 433 | $paramClassName = isset($reflectionParamClass) ? $reflectionParamClass->getName() : null; 434 | 435 | if (class_exists($paramClassName, true)) { 436 | // 找到类型走 typeMap 437 | $paramInstance = $this->buildByTypeRecursive($paramClassName, $stack); 438 | $realParams[$reflectionParam->getPosition()] = $paramInstance; 439 | } else { 440 | // 找不到类型走 aliasMap 441 | // FIXME 这个判断方法可以用, 但是不严谨 442 | $parameterName = $reflectionParam->getName(); 443 | $realParams[$reflectionParam->getPosition()] = $this->getByAlias($parameterName); 444 | } 445 | } 446 | 447 | if ($reflectionFunc instanceof ReflectionMethod) { 448 | $instance = $reflectionClass->newInstanceArgs($realParams); 449 | return $instance; 450 | } else { 451 | $result = $reflectionFunc->invokeArgs($realParams); 452 | } 453 | 454 | return $result; 455 | } 456 | 457 | } -------------------------------------------------------------------------------- /src/Di/ElementDefinition.php: -------------------------------------------------------------------------------- 1 | type; 64 | } 65 | 66 | /** 67 | * 获得这个元素定义的别名 68 | * @return string 69 | */ 70 | public function getAlias() 71 | { 72 | return $this->alias; 73 | } 74 | 75 | /** 76 | * 获得创建实例的回调函数 77 | * @return callable 78 | */ 79 | public function getBuilder() 80 | { 81 | return $this->builder; 82 | } 83 | 84 | /** 85 | * 获取实例 (仅在作用域 = SCOPE_PROTOTYPE) 时有效 86 | * @return mixed 87 | */ 88 | public function getInstance() 89 | { 90 | return $this->instance; 91 | } 92 | 93 | /** 94 | * 获取是否是单例作用域 95 | * @return bool 96 | */ 97 | public function isSingletonScope() 98 | { 99 | return $this->scope === self::SCOPE_SINGLETON; 100 | } 101 | 102 | /** 103 | * 获取是否是原型作用域 104 | * @return bool 105 | */ 106 | public function isPrototypeScope() 107 | { 108 | return $this->scope === self::SCOPE_PROTOTYPE; 109 | } 110 | 111 | /** 112 | * 返回这个定义是否延迟初始化 113 | * @return bool 114 | */ 115 | public function isDeferred() 116 | { 117 | return $this->deferred; 118 | } 119 | 120 | 121 | /** 122 | * 返回这个定义是否立即初始化 123 | * @return bool 124 | */ 125 | public function isEager() 126 | { 127 | return !$this->deferred; 128 | } 129 | 130 | /** 131 | * 判断是否在使用构造方法为构造回调函数 132 | * @return boolean 133 | */ 134 | public function isBuilderEqualsConstructor() 135 | { 136 | return $this->builder === self::NAME_OF_CONSTRUCTOR; 137 | } 138 | 139 | /** 140 | * 返回这个是否是一个基础类型 141 | * @return bool 142 | */ 143 | public function isBaseType() 144 | { 145 | return $this->baseType; 146 | } 147 | 148 | /** 149 | * 返回是否已经存在一个实例 150 | * @return bool 151 | */ 152 | public function isInstanceNull() 153 | { 154 | return is_null($this->instance); 155 | } 156 | 157 | /** 158 | * 设置类型, 如果是基础类型, 请使用 ElementDefinition::TYPE_* 定义的类型 159 | * @param string $type 160 | * @return ElementDefinition 161 | */ 162 | public function setType($type) 163 | { 164 | if (in_array($type, self::BASE_TYPES)) { 165 | $this->baseType = true; 166 | } else { 167 | $this->baseType = false; 168 | } 169 | $this->type = $type; 170 | return $this; 171 | } 172 | 173 | /** 174 | * 设置这个元素定义的别名 175 | * @param string $alias 176 | * @return $this 177 | */ 178 | public function setAlias($alias) 179 | { 180 | $this->alias = $alias; 181 | return $this; 182 | } 183 | 184 | /** 185 | * 设置为单例作用域 186 | * @return $this 187 | */ 188 | public function setSingletonScope() 189 | { 190 | $this->scope = self::SCOPE_SINGLETON; 191 | return $this; 192 | } 193 | 194 | /** 195 | * 设置为原型作用域 196 | * @return $this 197 | */ 198 | public function setPrototypeScope() 199 | { 200 | $this->setDeferred(); 201 | $this->scope = self::SCOPE_PROTOTYPE; 202 | return $this; 203 | } 204 | 205 | /** 206 | * 设置这个定义是否延迟初始化 207 | * @return $this 208 | */ 209 | public function setDeferred() 210 | { 211 | $this->deferred = true; 212 | return $this; 213 | } 214 | 215 | /** 216 | * 设置这个定义立即初始化 217 | * @return $this 218 | */ 219 | public function setEager() 220 | { 221 | $this->deferred = false; 222 | return $this; 223 | } 224 | /** 225 | * 设置创建实例的回调函数 226 | * @param callable $builder 227 | * @return $this 228 | */ 229 | public function setBuilder($builder) 230 | { 231 | $this->builder = $builder; 232 | return $this; 233 | } 234 | 235 | /** 236 | * 直接设置类的构造方法为构造回调函数 237 | * @return $this 238 | */ 239 | public function setBuilderToConstructor() 240 | { 241 | if ($this->builder != self::NAME_OF_CONSTRUCTOR) { 242 | $this->builder = self::NAME_OF_CONSTRUCTOR; 243 | } 244 | return $this; 245 | } 246 | 247 | /** 248 | * 设置实例 (会隐式的将 作用域 设置为单例) 249 | * @param mixed $instance 250 | * @return $this 251 | */ 252 | public function setInstance($instance) 253 | { 254 | $this->instance = $instance; 255 | $this->setSingletonScope(); 256 | return $this; 257 | } 258 | 259 | } -------------------------------------------------------------------------------- /src/Dispatcher/Dispatcher.php: -------------------------------------------------------------------------------- 1 | container = $container; 22 | } 23 | 24 | public function dispatch(Route $route, ServerRequestInterface $request) 25 | { 26 | $this->assertHasTarget($route); 27 | $target = $route->getTarget(); 28 | 29 | $callback = null; 30 | if (is_string($target) and class_exists($target, true)) { 31 | $pathComponents = explode('/', $request->getUri()->getPath()); 32 | $lastComponent = array_pop($pathComponents); 33 | //尝试从容器中获取对象 34 | $obj = $this->container->getByType($target); 35 | $callback = [$obj, $lastComponent]; 36 | } elseif (is_callable($target) and is_array($target) and count($target) == 2) { 37 | $class = array_shift($target); 38 | $method = array_shift($target); 39 | 40 | if (is_object($class)) { 41 | $callback = [$class, $method]; 42 | } else { 43 | //尝试从容器中获取对象 44 | $obj = $this->container->getByType($class); 45 | $callback = [$obj, $method]; 46 | } 47 | } elseif ($target instanceof Closure) { 48 | $callback = $target; 49 | } else { 50 | throw new DispatcherException(sprintf('匹配的路由规则 %s 的路由目标无效', $route->getUrlRule())); 51 | } 52 | 53 | return $callback; 54 | } 55 | 56 | private function assertHasTarget(Route $route) 57 | { 58 | if (!$route->hasTarget()) { 59 | throw new DispatcherException('匹配的路由规则没有路由目标'); 60 | } 61 | } 62 | 63 | 64 | } -------------------------------------------------------------------------------- /src/Exceptions/ContainerException.php: -------------------------------------------------------------------------------- 1 | '', 40 | 'domain' => null, 41 | 'hostonly' => null, 42 | 'path' => null, 43 | 'expires' => null, 44 | 'secure' => false, 45 | 'httponly' => false 46 | ]; 47 | 48 | /** 49 | * Create new cookies helper 50 | * 51 | * @param array $cookies 52 | */ 53 | public function __construct(array $cookies = []) 54 | { 55 | $this->requestCookies = $cookies; 56 | } 57 | 58 | /** 59 | * Set default cookie properties 60 | * 61 | * @param array $settings 62 | */ 63 | public function setDefaults(array $settings) 64 | { 65 | $this->defaults = array_replace($this->defaults, $settings); 66 | } 67 | 68 | /** 69 | * Get request cookie 70 | * 71 | * @param string $name Cookie name 72 | * @param mixed $default Cookie default value 73 | * 74 | * @return mixed Cookie value if present, else default 75 | */ 76 | public function get($name, $default = null) 77 | { 78 | return isset($this->requestCookies[$name]) ? $this->requestCookies[$name] : $default; 79 | } 80 | 81 | /** 82 | * Set response cookie 83 | * 84 | * @param string $name Cookie name 85 | * @param string|array $value Cookie value, or cookie properties 86 | */ 87 | public function set($name, $value) 88 | { 89 | if (!is_array($value)) { 90 | $value = ['value' => (string)$value]; 91 | } 92 | $this->responseCookies[$name] = array_replace($this->defaults, $value); 93 | } 94 | 95 | /** 96 | * Convert to `Set-Cookie` headers 97 | * 98 | * @return string[] 99 | */ 100 | public function toHeaders() 101 | { 102 | $headers = []; 103 | foreach ($this->responseCookies as $name => $properties) { 104 | $headers[] = $this->toHeader($name, $properties); 105 | } 106 | 107 | return $headers; 108 | } 109 | 110 | /** 111 | * Convert to `Set-Cookie` header 112 | * 113 | * @param string $name Cookie name 114 | * @param array $properties Cookie properties 115 | * 116 | * @return string 117 | */ 118 | protected function toHeader($name, array $properties) 119 | { 120 | $result = urlencode($name) . '=' . urlencode($properties['value']); 121 | 122 | if (isset($properties['domain'])) { 123 | $result .= '; domain=' . $properties['domain']; 124 | } 125 | 126 | if (isset($properties['path'])) { 127 | $result .= '; path=' . $properties['path']; 128 | } 129 | 130 | if (isset($properties['expires'])) { 131 | if (is_string($properties['expires'])) { 132 | $timestamp = strtotime($properties['expires']); 133 | } else { 134 | $timestamp = (int)$properties['expires']; 135 | } 136 | if ($timestamp !== 0) { 137 | $result .= '; expires=' . gmdate('D, d-M-Y H:i:s e', $timestamp); 138 | } 139 | } 140 | 141 | if (isset($properties['secure']) && $properties['secure']) { 142 | $result .= '; secure'; 143 | } 144 | 145 | if (isset($properties['hostonly']) && $properties['hostonly']) { 146 | $result .= '; HostOnly'; 147 | } 148 | 149 | if (isset($properties['httponly']) && $properties['httponly']) { 150 | $result .= '; HttpOnly'; 151 | } 152 | 153 | return $result; 154 | } 155 | 156 | /** 157 | * Parse HTTP request `Cookie:` header and extract 158 | * into a PHP associative array. 159 | * 160 | * @param string $header The raw HTTP request `Cookie:` header 161 | * 162 | * @return array Associative array of cookie names and values 163 | * 164 | * @throws InvalidArgumentException if the cookie data cannot be parsed 165 | */ 166 | public static function parseHeader($header) 167 | { 168 | if (is_array($header) === true) { 169 | $header = isset($header[0]) ? $header[0] : ''; 170 | } 171 | 172 | if (is_string($header) === false) { 173 | throw new InvalidArgumentException('Cannot parse Cookie data. Header value must be a string.'); 174 | } 175 | 176 | $header = rtrim($header, "\r\n"); 177 | $pieces = preg_split('@[;]\s*@', $header); 178 | $cookies = []; 179 | 180 | foreach ($pieces as $cookie) { 181 | $cookie = explode('=', $cookie, 2); 182 | 183 | if (count($cookie) === 2) { 184 | $key = urldecode($cookie[0]); 185 | $value = urldecode($cookie[1]); 186 | 187 | if (!isset($cookies[$key])) { 188 | $cookies[$key] = $value; 189 | } 190 | } 191 | } 192 | 193 | return $cookies; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Http/Headers.php: -------------------------------------------------------------------------------- 1 | 1, 38 | 'CONTENT_LENGTH' => 1, 39 | 'PHP_AUTH_USER' => 1, 40 | 'PHP_AUTH_PW' => 1, 41 | 'PHP_AUTH_DIGEST' => 1, 42 | 'AUTH_TYPE' => 1, 43 | ]; 44 | 45 | /** 46 | * Create new headers collection with data extracted from 47 | * the application Environment object 48 | * 49 | * @param Environment $environment The Slim application Environment 50 | * 51 | * @return self 52 | */ 53 | public static function createFromEnvironment(Environment $environment) 54 | { 55 | $data = []; 56 | $environment = self::determineAuthorization($environment); 57 | foreach ($environment as $key => $value) { 58 | $key = strtoupper($key); 59 | if (isset(static::$special[$key]) || strpos($key, 'HTTP_') === 0) { 60 | if ($key !== 'HTTP_CONTENT_LENGTH') { 61 | $data[$key] = $value; 62 | } 63 | } 64 | } 65 | 66 | return new static($data); 67 | } 68 | 69 | /** 70 | * If HTTP_AUTHORIZATION does not exist tries to get it from 71 | * getallheaders() when available. 72 | * 73 | * @param Environment $environment The Slim application Environment 74 | * 75 | * @return Environment 76 | */ 77 | 78 | public static function determineAuthorization(Environment $environment) 79 | { 80 | $authorization = $environment->get('HTTP_AUTHORIZATION'); 81 | 82 | if (null === $authorization && is_callable('getallheaders')) { 83 | $headers = getallheaders(); 84 | $headers = array_change_key_case($headers, CASE_LOWER); 85 | if (isset($headers['authorization'])) { 86 | $environment->set('HTTP_AUTHORIZATION', $headers['authorization']); 87 | } 88 | } 89 | 90 | return $environment; 91 | } 92 | 93 | /** 94 | * Return array of HTTP header names and values. 95 | * This method returns the _original_ header name 96 | * as specified by the end user. 97 | * 98 | * @return array 99 | */ 100 | public function getAll() 101 | { 102 | $all = parent::getAll(); 103 | $out = []; 104 | foreach ($all as $key => $props) { 105 | $out[$props['originalKey']] = $props['value']; 106 | } 107 | 108 | return $out; 109 | } 110 | 111 | /** 112 | * Set HTTP header value 113 | * 114 | * This method sets a header value. It replaces 115 | * any values that may already exist for the header name. 116 | * 117 | * @param string $key The case-insensitive header name 118 | * @param string $value The header value 119 | */ 120 | public function set($key, $value) 121 | { 122 | if (!is_array($value)) { 123 | $value = [$value]; 124 | } 125 | parent::set($this->normalizeKey($key), [ 126 | 'value' => $value, 127 | 'originalKey' => $key 128 | ]); 129 | } 130 | 131 | /** 132 | * Get HTTP header value 133 | * 134 | * @param string $key The case-insensitive header name 135 | * @param mixed $default The default value if key does not exist 136 | * 137 | * @return string[] 138 | */ 139 | public function get($key, $default = null) 140 | { 141 | if ($this->has($key)) { 142 | return parent::get($this->normalizeKey($key))['value']; 143 | } 144 | 145 | return $default; 146 | } 147 | 148 | /** 149 | * Get HTTP header key as originally specified 150 | * 151 | * @param string $key The case-insensitive header name 152 | * @param mixed $default The default value if key does not exist 153 | * 154 | * @return string 155 | */ 156 | public function getOriginalKey($key, $default = null) 157 | { 158 | if ($this->has($key)) { 159 | return parent::get($this->normalizeKey($key))['originalKey']; 160 | } 161 | 162 | return $default; 163 | } 164 | 165 | /** 166 | * Add HTTP header value 167 | * 168 | * This method appends a header value. Unlike the set() method, 169 | * this method _appends_ this new value to any values 170 | * that already exist for this header name. 171 | * 172 | * @param string $key The case-insensitive header name 173 | * @param array|string $value The new header value(s) 174 | */ 175 | public function add($key, $value) 176 | { 177 | $oldValues = $this->get($key, []); 178 | $newValues = is_array($value) ? $value : [$value]; 179 | $this->set($key, array_merge($oldValues, array_values($newValues))); 180 | } 181 | 182 | /** 183 | * Does this collection have a given header? 184 | * 185 | * @param string $key The case-insensitive header name 186 | * 187 | * @return bool 188 | */ 189 | public function has($key) 190 | { 191 | return parent::has($this->normalizeKey($key)); 192 | } 193 | 194 | /** 195 | * Remove header from collection 196 | * 197 | * @param string $key The case-insensitive header name 198 | */ 199 | public function remove($key) 200 | { 201 | parent::remove($this->normalizeKey($key)); 202 | } 203 | 204 | /** 205 | * Normalize header name 206 | * 207 | * This method transforms header names into a 208 | * normalized form. This is how we enable case-insensitive 209 | * header names in the other methods in this class. 210 | * 211 | * @param string $key The case-insensitive header name 212 | * 213 | * @return string Normalized header name 214 | */ 215 | public function normalizeKey($key) 216 | { 217 | $key = strtr(strtolower($key), '_', '-'); 218 | if (strpos($key, 'http-') === 0) { 219 | $key = substr($key, 5); 220 | } 221 | 222 | return $key; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/Http/Message.php: -------------------------------------------------------------------------------- 1 | true, 41 | '1.1' => true, 42 | '2.0' => true, 43 | ]; 44 | 45 | /** 46 | * Headers 47 | * 48 | * @var \Smile\Interfaces\Http\HeadersInterface 49 | */ 50 | protected $headers; 51 | 52 | /** 53 | * Body object 54 | * 55 | * @var \Psr\Http\Message\StreamInterface 56 | */ 57 | protected $body; 58 | 59 | 60 | /** 61 | * Disable magic setter to ensure immutability 62 | */ 63 | public function __set($name, $value) 64 | { 65 | return; 66 | } 67 | 68 | /******************************************************************************* 69 | * Protocol 70 | ******************************************************************************/ 71 | 72 | /** 73 | * Retrieves the Http protocol version as a string. 74 | * 75 | * The string MUST contain only the Http version number (e.g., "1.1", "1.0"). 76 | * 77 | * @return string Http protocol version. 78 | */ 79 | public function getProtocolVersion() 80 | { 81 | return $this->protocolVersion; 82 | } 83 | 84 | /** 85 | * Return an instance with the specified Http protocol version. 86 | * 87 | * The version string MUST contain only the Http version number (e.g., 88 | * "1.1", "1.0"). 89 | * 90 | * This method MUST be implemented in such a way as to retain the 91 | * immutability of the message, and MUST return an instance that has the 92 | * new protocol version. 93 | * 94 | * @param string $version Http protocol version 95 | * @return static 96 | * @throws InvalidArgumentException if the http version is an invalid number 97 | */ 98 | public function withProtocolVersion($version) 99 | { 100 | if (!isset(self::$validProtocolVersions[$version])) { 101 | throw new InvalidArgumentException( 102 | 'Invalid Http version. Must be one of: ' 103 | . implode(', ', array_keys(self::$validProtocolVersions)) 104 | ); 105 | } 106 | $clone = clone $this; 107 | $clone->protocolVersion = $version; 108 | 109 | return $clone; 110 | } 111 | 112 | /******************************************************************************* 113 | * Headers 114 | ******************************************************************************/ 115 | 116 | /** 117 | * Retrieves all message header values. 118 | * 119 | * The keys represent the header name as it will be sent over the wire, and 120 | * each value is an array of strings associated with the header. 121 | * 122 | * // Represent the headers as a string 123 | * foreach ($message->getHeaders() as $name => $values) { 124 | * echo $name . ": " . implode(", ", $values); 125 | * } 126 | * 127 | * // Emit headers iteratively: 128 | * foreach ($message->getHeaders() as $name => $values) { 129 | * foreach ($values as $value) { 130 | * header(sprintf('%s: %s', $name, $value), false); 131 | * } 132 | * } 133 | * 134 | * While header names are not case-sensitive, getHeaders() will preserve the 135 | * exact case in which headers were originally specified. 136 | * 137 | * @return array Returns an associative array of the message's headers. Each 138 | * key MUST be a header name, and each value MUST be an array of strings 139 | * for that header. 140 | */ 141 | public function getHeaders() 142 | { 143 | return $this->headers->getAll(); 144 | } 145 | 146 | /** 147 | * Checks if a header exists by the given case-insensitive name. 148 | * 149 | * @param string $name Case-insensitive header field name. 150 | * @return bool Returns true if any header names match the given header 151 | * name using a case-insensitive string comparison. Returns false if 152 | * no matching header name is found in the message. 153 | */ 154 | public function hasHeader($name) 155 | { 156 | return $this->headers->has($name); 157 | } 158 | 159 | /** 160 | * Retrieves a message header value by the given case-insensitive name. 161 | * 162 | * This method returns an array of all the header values of the given 163 | * case-insensitive header name. 164 | * 165 | * If the header does not appear in the message, this method MUST return an 166 | * empty array. 167 | * 168 | * @param string $name Case-insensitive header field name. 169 | * @return string[] An array of string values as provided for the given 170 | * header. If the header does not appear in the message, this method MUST 171 | * return an empty array. 172 | */ 173 | public function getHeader($name) 174 | { 175 | return $this->headers->get($name, []); 176 | } 177 | 178 | /** 179 | * Retrieves a comma-separated string of the values for a single header. 180 | * 181 | * This method returns all of the header values of the given 182 | * case-insensitive header name as a string concatenated together using 183 | * a comma. 184 | * 185 | * NOTE: Not all header values may be appropriately represented using 186 | * comma concatenation. For such headers, use getHeader() instead 187 | * and supply your own delimiter when concatenating. 188 | * 189 | * If the header does not appear in the message, this method MUST return 190 | * an empty string. 191 | * 192 | * @param string $name Case-insensitive header field name. 193 | * @return string A string of values as provided for the given header 194 | * concatenated together using a comma. If the header does not appear in 195 | * the message, this method MUST return an empty string. 196 | */ 197 | public function getHeaderLine($name) 198 | { 199 | return implode(',', $this->headers->get($name, [])); 200 | } 201 | 202 | /** 203 | * Return an instance with the provided value replacing the specified header. 204 | * 205 | * While header names are case-insensitive, the casing of the header will 206 | * be preserved by this function, and returned from getHeaders(). 207 | * 208 | * This method MUST be implemented in such a way as to retain the 209 | * immutability of the message, and MUST return an instance that has the 210 | * new and/or updated header and value. 211 | * 212 | * @param string $name Case-insensitive header field name. 213 | * @param string|string[] $value Header value(s). 214 | * @return static 215 | * @throws \InvalidArgumentException for invalid header names or values. 216 | */ 217 | public function withHeader($name, $value) 218 | { 219 | $clone = clone $this; 220 | $clone->headers->set($name, $value); 221 | 222 | return $clone; 223 | } 224 | 225 | /** 226 | * Return an instance with the specified header appended with the given value. 227 | * 228 | * Existing values for the specified header will be maintained. The new 229 | * value(s) will be appended to the existing list. If the header did not 230 | * exist previously, it will be added. 231 | * 232 | * This method MUST be implemented in such a way as to retain the 233 | * immutability of the message, and MUST return an instance that has the 234 | * new header and/or value. 235 | * 236 | * @param string $name Case-insensitive header field name to add. 237 | * @param string|string[] $value Header value(s). 238 | * @return static 239 | * @throws \InvalidArgumentException for invalid header names or values. 240 | */ 241 | public function withAddedHeader($name, $value) 242 | { 243 | $clone = clone $this; 244 | $clone->headers->add($name, $value); 245 | 246 | return $clone; 247 | } 248 | 249 | /** 250 | * Return an instance without the specified header. 251 | * 252 | * Header resolution MUST be done without case-sensitivity. 253 | * 254 | * This method MUST be implemented in such a way as to retain the 255 | * immutability of the message, and MUST return an instance that removes 256 | * the named header. 257 | * 258 | * @param string $name Case-insensitive header field name to remove. 259 | * @return static 260 | */ 261 | public function withoutHeader($name) 262 | { 263 | $clone = clone $this; 264 | $clone->headers->remove($name); 265 | 266 | return $clone; 267 | } 268 | 269 | /******************************************************************************* 270 | * Body 271 | ******************************************************************************/ 272 | 273 | /** 274 | * Gets the body of the message. 275 | * 276 | * @return StreamInterface Returns the body as a stream. 277 | */ 278 | public function getBody() 279 | { 280 | return $this->body; 281 | } 282 | 283 | /** 284 | * Return an instance with the specified message body. 285 | * 286 | * The body MUST be a StreamInterface object. 287 | * 288 | * This method MUST be implemented in such a way as to retain the 289 | * immutability of the message, and MUST return a new instance that has the 290 | * new body stream. 291 | * 292 | * @param StreamInterface $body Body. 293 | * @return static 294 | * @throws \InvalidArgumentException When the body is not valid. 295 | */ 296 | public function withBody(StreamInterface $body) 297 | { 298 | // TODO: Test for invalid body? 299 | $clone = clone $this; 300 | $clone->body = $body; 301 | 302 | return $clone; 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /src/Http/Request.php: -------------------------------------------------------------------------------- 1 | 1, 119 | 'DELETE' => 1, 120 | 'GET' => 1, 121 | 'HEAD' => 1, 122 | 'OPTIONS' => 1, 123 | 'PATCH' => 1, 124 | 'POST' => 1, 125 | 'PUT' => 1, 126 | 'TRACE' => 1, 127 | ]; 128 | 129 | /** 130 | * Create new Http request with data extracted from the application 131 | * Environment object 132 | * 133 | * @param Environment $environment The Slim application Environment 134 | * 135 | * @return self 136 | */ 137 | public static function createFromEnvironment(Environment $environment) 138 | { 139 | $method = $environment['REQUEST_METHOD']; 140 | $uri = Uri::createFromEnvironment($environment); 141 | $headers = Headers::createFromEnvironment($environment); 142 | $cookies = Cookies::parseHeader($headers->get('Cookie', [])); 143 | $serverParams = $environment->getAll(); 144 | $body = new RequestBody(); 145 | $uploadedFiles = UploadedFile::createFromEnvironment($environment); 146 | 147 | $request = new static($method, $uri, $headers, $cookies, $serverParams, $body, $uploadedFiles); 148 | 149 | if ($method === 'POST' && 150 | in_array($request->getMediaType(), ['application/x-www-form-urlencoded', 'multipart/form-data']) 151 | ) { 152 | // parsed body must be $_POST 153 | $request = $request->withParsedBody($_POST); 154 | } 155 | return $request; 156 | } 157 | 158 | /** 159 | * Create new Http request. 160 | * 161 | * Adds a host header when none was provided and a host is defined in uri. 162 | * 163 | * @param string $method The request method 164 | * @param UriInterface $uri The request URI object 165 | * @param HeadersInterface $headers The request headers collection 166 | * @param array $cookies The request cookies collection 167 | * @param array $serverParams The server environment variables 168 | * @param StreamInterface $body The request body object 169 | * @param array $uploadedFiles The request uploadedFiles collection 170 | */ 171 | public function __construct( 172 | $method, 173 | UriInterface $uri, 174 | HeadersInterface $headers, 175 | array $cookies, 176 | array $serverParams, 177 | StreamInterface $body, 178 | array $uploadedFiles = [] 179 | ) { 180 | $this->originalMethod = $this->filterMethod($method); 181 | $this->uri = $uri; 182 | $this->headers = $headers; 183 | $this->cookies = $cookies; 184 | $this->serverParams = $serverParams; 185 | $this->attributes = new Map(); 186 | $this->body = $body; 187 | $this->uploadedFiles = $uploadedFiles; 188 | 189 | if (isset($serverParams['SERVER_PROTOCOL'])) { 190 | $this->protocolVersion = str_replace('Http/', '', $serverParams['SERVER_PROTOCOL']); 191 | } 192 | 193 | if (!$this->headers->has('Host') || $this->uri->getHost() !== '') { 194 | $this->headers->set('Host', $this->uri->getHost()); 195 | } 196 | 197 | $this->registerMediaTypeParser('application/json', function ($input) { 198 | return json_decode($input, true); 199 | }); 200 | 201 | $this->registerMediaTypeParser('application/xml', function ($input) { 202 | $backup = libxml_disable_entity_loader(true); 203 | $result = simplexml_load_string($input); 204 | libxml_disable_entity_loader($backup); 205 | return $result; 206 | }); 207 | 208 | $this->registerMediaTypeParser('text/xml', function ($input) { 209 | $backup = libxml_disable_entity_loader(true); 210 | $result = simplexml_load_string($input); 211 | libxml_disable_entity_loader($backup); 212 | return $result; 213 | }); 214 | 215 | $this->registerMediaTypeParser('application/x-www-form-urlencoded', function ($input) { 216 | parse_str($input, $data); 217 | return $data; 218 | }); 219 | } 220 | 221 | /** 222 | * This method is applied to the cloned object 223 | * after PHP performs an initial shallow-copy. This 224 | * method completes a deep-copy by creating new objects 225 | * for the cloned object's internal reference pointers. 226 | */ 227 | public function __clone() 228 | { 229 | $this->headers = clone $this->headers; 230 | $this->attributes = clone $this->attributes; 231 | $this->body = clone $this->body; 232 | } 233 | 234 | /******************************************************************************* 235 | * Method 236 | ******************************************************************************/ 237 | 238 | /** 239 | * Retrieves the Http method of the request. 240 | * 241 | * @return string Returns the request method. 242 | */ 243 | public function getMethod() 244 | { 245 | if ($this->method === null) { 246 | $this->method = $this->originalMethod; 247 | $customMethod = $this->getHeaderLine('X-Http-Method-Override'); 248 | 249 | if ($customMethod) { 250 | $this->method = $this->filterMethod($customMethod); 251 | } elseif ($this->originalMethod === 'POST') { 252 | $overrideMethod = $this->filterMethod($this->getParsedBodyParam('_METHOD')); 253 | if ($overrideMethod !== null) { 254 | $this->method = $overrideMethod; 255 | } 256 | 257 | if ($this->getBody()->eof()) { 258 | $this->getBody()->rewind(); 259 | } 260 | } 261 | } 262 | 263 | return $this->method; 264 | } 265 | 266 | /** 267 | * Get the original Http method (ignore override). 268 | * 269 | * Note: This method is not part of the PSR-7 standard. 270 | * 271 | * @return string 272 | */ 273 | public function getOriginalMethod() 274 | { 275 | return $this->originalMethod; 276 | } 277 | 278 | /** 279 | * Return an instance with the provided Http method. 280 | * 281 | * While Http method names are typically all uppercase characters, Http 282 | * method names are case-sensitive and thus implementations SHOULD NOT 283 | * modify the given string. 284 | * 285 | * This method MUST be implemented in such a way as to retain the 286 | * immutability of the message, and MUST return an instance that has the 287 | * changed request method. 288 | * 289 | * @param string $method Case-sensitive method. 290 | * @return self 291 | * @throws \InvalidArgumentException for invalid Http methods. 292 | */ 293 | public function withMethod($method) 294 | { 295 | $method = $this->filterMethod($method); 296 | $clone = clone $this; 297 | $clone->originalMethod = $method; 298 | $clone->method = $method; 299 | 300 | return $clone; 301 | } 302 | 303 | /** 304 | * Validate the Http method 305 | * 306 | * @param null|string $method 307 | * @return null|string 308 | * @throws \InvalidArgumentException on invalid Http method. 309 | */ 310 | protected function filterMethod($method) 311 | { 312 | if ($method === null) { 313 | return $method; 314 | } 315 | 316 | if (!is_string($method)) { 317 | throw new InvalidArgumentException(sprintf( 318 | 'Unsupported Http method; must be a string, received %s', 319 | (is_object($method) ? get_class($method) : gettype($method)) 320 | )); 321 | } 322 | 323 | $method = strtoupper($method); 324 | if (!isset($this->validMethods[$method])) { 325 | throw new InvalidArgumentException(sprintf( 326 | 'Unsupported Http method "%s" provided', 327 | $method 328 | )); 329 | } 330 | 331 | return $method; 332 | } 333 | 334 | /** 335 | * Does this request use a given method? 336 | * 337 | * Note: This method is not part of the PSR-7 standard. 338 | * 339 | * @param string $method Http method 340 | * @return bool 341 | */ 342 | public function isMethod($method) 343 | { 344 | return $this->getMethod() === $method; 345 | } 346 | 347 | /** 348 | * Is this a GET request? 349 | * 350 | * Note: This method is not part of the PSR-7 standard. 351 | * 352 | * @return bool 353 | */ 354 | public function isGet() 355 | { 356 | return $this->isMethod('GET'); 357 | } 358 | 359 | /** 360 | * Is this a POST request? 361 | * 362 | * Note: This method is not part of the PSR-7 standard. 363 | * 364 | * @return bool 365 | */ 366 | public function isPost() 367 | { 368 | return $this->isMethod('POST'); 369 | } 370 | 371 | /** 372 | * Is this a PUT request? 373 | * 374 | * Note: This method is not part of the PSR-7 standard. 375 | * 376 | * @return bool 377 | */ 378 | public function isPut() 379 | { 380 | return $this->isMethod('PUT'); 381 | } 382 | 383 | /** 384 | * Is this a PATCH request? 385 | * 386 | * Note: This method is not part of the PSR-7 standard. 387 | * 388 | * @return bool 389 | */ 390 | public function isPatch() 391 | { 392 | return $this->isMethod('PATCH'); 393 | } 394 | 395 | /** 396 | * Is this a DELETE request? 397 | * 398 | * Note: This method is not part of the PSR-7 standard. 399 | * 400 | * @return bool 401 | */ 402 | public function isDelete() 403 | { 404 | return $this->isMethod('DELETE'); 405 | } 406 | 407 | /** 408 | * Is this a HEAD request? 409 | * 410 | * Note: This method is not part of the PSR-7 standard. 411 | * 412 | * @return bool 413 | */ 414 | public function isHead() 415 | { 416 | return $this->isMethod('HEAD'); 417 | } 418 | 419 | /** 420 | * Is this a OPTIONS request? 421 | * 422 | * Note: This method is not part of the PSR-7 standard. 423 | * 424 | * @return bool 425 | */ 426 | public function isOptions() 427 | { 428 | return $this->isMethod('OPTIONS'); 429 | } 430 | 431 | /** 432 | * Is this an XHR request? 433 | * 434 | * Note: This method is not part of the PSR-7 standard. 435 | * 436 | * @return bool 437 | */ 438 | public function isXhr() 439 | { 440 | return $this->getHeaderLine('X-Requested-With') === 'XMLHttpRequest'; 441 | } 442 | 443 | /******************************************************************************* 444 | * URI 445 | ******************************************************************************/ 446 | 447 | /** 448 | * Retrieves the message's request target. 449 | * 450 | * Retrieves the message's request-target either as it will appear (for 451 | * clients), as it appeared at request (for servers), or as it was 452 | * specified for the instance (see withRequestTarget()). 453 | * 454 | * In most cases, this will be the origin-form of the composed URI, 455 | * unless a value was provided to the concrete implementation (see 456 | * withRequestTarget() below). 457 | * 458 | * If no URI is available, and no request-target has been specifically 459 | * provided, this method MUST return the string "/". 460 | * 461 | * @return string 462 | */ 463 | public function getRequestTarget() 464 | { 465 | if ($this->requestTarget) { 466 | return $this->requestTarget; 467 | } 468 | 469 | if ($this->uri === null) { 470 | return '/'; 471 | } 472 | 473 | $basePath = $this->uri->getBasePath(); 474 | $path = $this->uri->getPath(); 475 | $path = $basePath . '/' . ltrim($path, '/'); 476 | 477 | $query = $this->uri->getQuery(); 478 | if ($query) { 479 | $path .= '?' . $query; 480 | } 481 | $this->requestTarget = $path; 482 | 483 | return $this->requestTarget; 484 | } 485 | 486 | /** 487 | * Return an instance with the specific request-target. 488 | * 489 | * If the request needs a non-origin-form request-target — e.g., for 490 | * specifying an absolute-form, authority-form, or asterisk-form — 491 | * this method may be used to create an instance with the specified 492 | * request-target, verbatim. 493 | * 494 | * This method MUST be implemented in such a way as to retain the 495 | * immutability of the message, and MUST return an instance that has the 496 | * changed request target. 497 | * 498 | * @link http://tools.ietf.org/html/rfc7230#section-2.7 (for the various 499 | * request-target forms allowed in request messages) 500 | * @param mixed $requestTarget 501 | * @return self 502 | * @throws InvalidArgumentException if the request target is invalid 503 | */ 504 | public function withRequestTarget($requestTarget) 505 | { 506 | if (preg_match('#\s#', $requestTarget)) { 507 | throw new InvalidArgumentException( 508 | 'Invalid request target provided; must be a string and cannot contain whitespace' 509 | ); 510 | } 511 | $clone = clone $this; 512 | $clone->requestTarget = $requestTarget; 513 | 514 | return $clone; 515 | } 516 | 517 | /** 518 | * Retrieves the URI instance. 519 | * 520 | * This method MUST return a UriInterface instance. 521 | * 522 | * @link http://tools.ietf.org/html/rfc3986#section-4.3 523 | * @return UriInterface Returns a UriInterface instance 524 | * representing the URI of the request. 525 | */ 526 | public function getUri() 527 | { 528 | return $this->uri; 529 | } 530 | 531 | /** 532 | * Returns an instance with the provided URI. 533 | * 534 | * This method MUST update the Host header of the returned request by 535 | * default if the URI contains a host component. If the URI does not 536 | * contain a host component, any pre-existing Host header MUST be carried 537 | * over to the returned request. 538 | * 539 | * You can opt-in to preserving the original state of the Host header by 540 | * setting `$preserveHost` to `true`. When `$preserveHost` is set to 541 | * `true`, this method interacts with the Host header in the following ways: 542 | * 543 | * - If the the Host header is missing or empty, and the new URI contains 544 | * a host component, this method MUST update the Host header in the returned 545 | * request. 546 | * - If the Host header is missing or empty, and the new URI does not contain a 547 | * host component, this method MUST NOT update the Host header in the returned 548 | * request. 549 | * - If a Host header is present and non-empty, this method MUST NOT update 550 | * the Host header in the returned request. 551 | * 552 | * This method MUST be implemented in such a way as to retain the 553 | * immutability of the message, and MUST return an instance that has the 554 | * new UriInterface instance. 555 | * 556 | * @link http://tools.ietf.org/html/rfc3986#section-4.3 557 | * @param UriInterface $uri New request URI to use. 558 | * @param bool $preserveHost Preserve the original state of the Host header. 559 | * @return self 560 | */ 561 | public function withUri(UriInterface $uri, $preserveHost = false) 562 | { 563 | $clone = clone $this; 564 | $clone->uri = $uri; 565 | 566 | if (!$preserveHost) { 567 | if ($uri->getHost() !== '') { 568 | $clone->headers->set('Host', $uri->getHost()); 569 | } 570 | } else { 571 | if ($this->uri->getHost() !== '' && (!$this->hasHeader('Host') || $this->getHeader('Host') === null)) { 572 | $clone->headers->set('Host', $uri->getHost()); 573 | } 574 | } 575 | 576 | return $clone; 577 | } 578 | 579 | /** 580 | * Get request content type. 581 | * 582 | * Note: This method is not part of the PSR-7 standard. 583 | * 584 | * @return string|null The request content type, if known 585 | */ 586 | public function getContentType() 587 | { 588 | $result = $this->getHeader('Content-Type'); 589 | 590 | return $result ? $result[0] : null; 591 | } 592 | 593 | /** 594 | * Get request media type, if known. 595 | * 596 | * Note: This method is not part of the PSR-7 standard. 597 | * 598 | * @return string|null The request media type, minus content-type params 599 | */ 600 | public function getMediaType() 601 | { 602 | $contentType = $this->getContentType(); 603 | if ($contentType) { 604 | $contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType); 605 | 606 | return strtolower($contentTypeParts[0]); 607 | } 608 | 609 | return null; 610 | } 611 | 612 | /** 613 | * Get request media type params, if known. 614 | * 615 | * Note: This method is not part of the PSR-7 standard. 616 | * 617 | * @return array 618 | */ 619 | public function getMediaTypeParams() 620 | { 621 | $contentType = $this->getContentType(); 622 | $contentTypeParams = []; 623 | if ($contentType) { 624 | $contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType); 625 | $contentTypePartsLength = count($contentTypeParts); 626 | for ($i = 1; $i < $contentTypePartsLength; $i++) { 627 | $paramParts = explode('=', $contentTypeParts[$i]); 628 | $contentTypeParams[strtolower($paramParts[0])] = $paramParts[1]; 629 | } 630 | } 631 | 632 | return $contentTypeParams; 633 | } 634 | 635 | /** 636 | * Get request content character set, if known. 637 | * 638 | * Note: This method is not part of the PSR-7 standard. 639 | * 640 | * @return string|null 641 | */ 642 | public function getContentCharset() 643 | { 644 | $mediaTypeParams = $this->getMediaTypeParams(); 645 | if (isset($mediaTypeParams['charset'])) { 646 | return $mediaTypeParams['charset']; 647 | } 648 | 649 | return null; 650 | } 651 | 652 | /** 653 | * Get request content length, if known. 654 | * 655 | * Note: This method is not part of the PSR-7 standard. 656 | * 657 | * @return int|null 658 | */ 659 | public function getContentLength() 660 | { 661 | $result = $this->headers->get('Content-Length'); 662 | 663 | return $result ? (int)$result[0] : null; 664 | } 665 | 666 | /******************************************************************************* 667 | * Cookies 668 | ******************************************************************************/ 669 | 670 | /** 671 | * Retrieve cookies. 672 | * 673 | * Retrieves cookies sent by the client to the server. 674 | * 675 | * The data MUST be compatible with the structure of the $_COOKIE 676 | * superglobal. 677 | * 678 | * @return array 679 | */ 680 | public function getCookieParams() 681 | { 682 | return $this->cookies; 683 | } 684 | 685 | /** 686 | * Fetch cookie value from cookies sent by the client to the server. 687 | * 688 | * Note: This method is not part of the PSR-7 standard. 689 | * 690 | * @param string $key The attribute name. 691 | * @param mixed $default Default value to return if the attribute does not exist. 692 | * 693 | * @return mixed 694 | */ 695 | public function getCookieParam($key, $default = null) 696 | { 697 | $cookies = $this->getCookieParams(); 698 | $result = $default; 699 | if (isset($cookies[$key])) { 700 | $result = $cookies[$key]; 701 | } 702 | 703 | return $result; 704 | } 705 | 706 | /** 707 | * Return an instance with the specified cookies. 708 | * 709 | * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST 710 | * be compatible with the structure of $_COOKIE. Typically, this data will 711 | * be injected at instantiation. 712 | * 713 | * This method MUST NOT update the related Cookie header of the request 714 | * instance, nor related values in the server params. 715 | * 716 | * This method MUST be implemented in such a way as to retain the 717 | * immutability of the message, and MUST return an instance that has the 718 | * updated cookie values. 719 | * 720 | * @param array $cookies Array of key/value pairs representing cookies. 721 | * @return self 722 | */ 723 | public function withCookieParams(array $cookies) 724 | { 725 | $clone = clone $this; 726 | $clone->cookies = $cookies; 727 | 728 | return $clone; 729 | } 730 | 731 | /******************************************************************************* 732 | * Query Params 733 | ******************************************************************************/ 734 | 735 | /** 736 | * Retrieve query string arguments. 737 | * 738 | * Retrieves the deserialized query string arguments, if any. 739 | * 740 | * Note: the query params might not be in sync with the URI or server 741 | * params. If you need to ensure you are only getting the original 742 | * values, you may need to parse the query string from `getUri()->getQuery()` 743 | * or from the `QUERY_STRING` server param. 744 | * 745 | * @return array 746 | */ 747 | public function getQueryParams() 748 | { 749 | if (is_array($this->queryParams)) { 750 | return $this->queryParams; 751 | } 752 | 753 | if ($this->uri === null) { 754 | return []; 755 | } 756 | 757 | parse_str($this->uri->getQuery(), $this->queryParams); // <-- URL decodes data 758 | 759 | return $this->queryParams; 760 | } 761 | 762 | /** 763 | * Return an instance with the specified query string arguments. 764 | * 765 | * These values SHOULD remain immutable over the course of the incoming 766 | * request. They MAY be injected during instantiation, such as from PHP's 767 | * $_GET superglobal, or MAY be derived from some other value such as the 768 | * URI. In cases where the arguments are parsed from the URI, the data 769 | * MUST be compatible with what PHP's parse_str() would return for 770 | * purposes of how duplicate query parameters are handled, and how nested 771 | * sets are handled. 772 | * 773 | * Setting query string arguments MUST NOT change the URI stored by the 774 | * request, nor the values in the server params. 775 | * 776 | * This method MUST be implemented in such a way as to retain the 777 | * immutability of the message, and MUST return an instance that has the 778 | * updated query string arguments. 779 | * 780 | * @param array $query Array of query string arguments, typically from 781 | * $_GET. 782 | * @return self 783 | */ 784 | public function withQueryParams(array $query) 785 | { 786 | $clone = clone $this; 787 | $clone->queryParams = $query; 788 | 789 | return $clone; 790 | } 791 | 792 | /******************************************************************************* 793 | * File Params 794 | ******************************************************************************/ 795 | 796 | /** 797 | * Retrieve normalized file upload data. 798 | * 799 | * This method returns upload metadata in a normalized tree, with each leaf 800 | * an instance of Psr\Http\Message\UploadedFileInterface. 801 | * 802 | * These values MAY be prepared from $_FILES or the message body during 803 | * instantiation, or MAY be injected via withUploadedFiles(). 804 | * 805 | * @return array An array tree of UploadedFileInterface instances; an empty 806 | * array MUST be returned if no data is present. 807 | */ 808 | public function getUploadedFiles() 809 | { 810 | return $this->uploadedFiles; 811 | } 812 | 813 | /** 814 | * Create a new instance with the specified uploaded files. 815 | * 816 | * This method MUST be implemented in such a way as to retain the 817 | * immutability of the message, and MUST return an instance that has the 818 | * updated body parameters. 819 | * 820 | * @param array $uploadedFiles An array tree of UploadedFileInterface instances. 821 | * @return self 822 | * @throws \InvalidArgumentException if an invalid structure is provided. 823 | */ 824 | public function withUploadedFiles(array $uploadedFiles) 825 | { 826 | $clone = clone $this; 827 | $clone->uploadedFiles = $uploadedFiles; 828 | 829 | return $clone; 830 | } 831 | 832 | /******************************************************************************* 833 | * Server Params 834 | ******************************************************************************/ 835 | 836 | /** 837 | * Retrieve server parameters. 838 | * 839 | * Retrieves data related to the incoming request environment, 840 | * typically derived from PHP's $_SERVER superglobal. The data IS NOT 841 | * REQUIRED to originate from $_SERVER. 842 | * 843 | * @return array 844 | */ 845 | public function getServerParams() 846 | { 847 | return $this->serverParams; 848 | } 849 | 850 | /** 851 | * Retrieve a server parameter. 852 | * 853 | * Note: This method is not part of the PSR-7 standard. 854 | * 855 | * @param string $key 856 | * @param mixed $default 857 | * @return mixed 858 | */ 859 | public function getServerParam($key, $default = null) 860 | { 861 | $serverParams = $this->getServerParams(); 862 | 863 | return isset($serverParams[$key]) ? $serverParams[$key] : $default; 864 | } 865 | 866 | /******************************************************************************* 867 | * Attributes 868 | ******************************************************************************/ 869 | 870 | /** 871 | * Retrieve attributes derived from the request. 872 | * 873 | * The request "attributes" may be used to allow injection of any 874 | * parameters derived from the request: e.g., the results of path 875 | * match operations; the results of decrypting cookies; the results of 876 | * deserializing non-form-encoded message bodies; etc. Attributes 877 | * will be application and request specific, and CAN be mutable. 878 | * 879 | * @return array Attributes derived from the request. 880 | */ 881 | public function getAttributes() 882 | { 883 | return $this->attributes->getAll(); 884 | } 885 | 886 | /** 887 | * Retrieve a single derived request attribute. 888 | * 889 | * Retrieves a single derived request attribute as described in 890 | * getAttributes(). If the attribute has not been previously set, returns 891 | * the default value as provided. 892 | * 893 | * This method obviates the need for a hasAttribute() method, as it allows 894 | * specifying a default value to return if the attribute is not found. 895 | * 896 | * @see getAttributes() 897 | * @param string $name The attribute name. 898 | * @param mixed $default Default value to return if the attribute does not exist. 899 | * @return mixed 900 | */ 901 | public function getAttribute($name, $default = null) 902 | { 903 | return $this->attributes->get($name, $default); 904 | } 905 | 906 | /** 907 | * Return an instance with the specified derived request attribute. 908 | * 909 | * This method allows setting a single derived request attribute as 910 | * described in getAttributes(). 911 | * 912 | * This method MUST be implemented in such a way as to retain the 913 | * immutability of the message, and MUST return an instance that has the 914 | * updated attribute. 915 | * 916 | * @see getAttributes() 917 | * @param string $name The attribute name. 918 | * @param mixed $value The value of the attribute. 919 | * @return self 920 | */ 921 | public function withAttribute($name, $value) 922 | { 923 | $clone = clone $this; 924 | $clone->attributes->set($name, $value); 925 | 926 | return $clone; 927 | } 928 | 929 | /** 930 | * Create a new instance with the specified derived request attributes. 931 | * 932 | * Note: This method is not part of the PSR-7 standard. 933 | * 934 | * This method allows setting all new derived request attributes as 935 | * described in getAttributes(). 936 | * 937 | * This method MUST be implemented in such a way as to retain the 938 | * immutability of the message, and MUST return a new instance that has the 939 | * updated attributes. 940 | * 941 | * @param array $attributes New attributes 942 | * @return self 943 | */ 944 | public function withAttributes(array $attributes) 945 | { 946 | $clone = clone $this; 947 | $clone->attributes = new Map($attributes); 948 | 949 | return $clone; 950 | } 951 | 952 | /** 953 | * Return an instance that removes the specified derived request attribute. 954 | * 955 | * This method allows removing a single derived request attribute as 956 | * described in getAttributes(). 957 | * 958 | * This method MUST be implemented in such a way as to retain the 959 | * immutability of the message, and MUST return an instance that removes 960 | * the attribute. 961 | * 962 | * @see getAttributes() 963 | * @param string $name The attribute name. 964 | * @return self 965 | */ 966 | public function withoutAttribute($name) 967 | { 968 | $clone = clone $this; 969 | $clone->attributes->remove($name); 970 | 971 | return $clone; 972 | } 973 | 974 | /******************************************************************************* 975 | * Body 976 | ******************************************************************************/ 977 | 978 | /** 979 | * Retrieve any parameters provided in the request body. 980 | * 981 | * If the request Content-Type is either application/x-www-form-urlencoded 982 | * or multipart/form-data, and the request method is POST, this method MUST 983 | * return the contents of $_POST. 984 | * 985 | * Otherwise, this method may return any results of deserializing 986 | * the request body content; as parsing returns structured content, the 987 | * potential types MUST be arrays or objects only. A null value indicates 988 | * the absence of body content. 989 | * 990 | * @return null|array|object The deserialized body parameters, if any. 991 | * These will typically be an array or object. 992 | * @throws RuntimeException if the request body media type parser returns an invalid value 993 | */ 994 | public function getParsedBody() 995 | { 996 | if ($this->bodyParsed !== false) { 997 | return $this->bodyParsed; 998 | } 999 | 1000 | if (!$this->body) { 1001 | return null; 1002 | } 1003 | 1004 | $mediaType = $this->getMediaType(); 1005 | 1006 | // look for a media type with a structured syntax suffix (RFC 6839) 1007 | $parts = explode('+', $mediaType); 1008 | if (count($parts) >= 2) { 1009 | $mediaType = 'application/' . $parts[count($parts)-1]; 1010 | } 1011 | 1012 | if (isset($this->bodyParsers[$mediaType]) === true) { 1013 | $body = (string)$this->getBody(); 1014 | $parsed = $this->bodyParsers[$mediaType]($body); 1015 | 1016 | if (!is_null($parsed) && !is_object($parsed) && !is_array($parsed)) { 1017 | throw new RuntimeException( 1018 | 'Request body media type parser return value must be an array, an object, or null' 1019 | ); 1020 | } 1021 | $this->bodyParsed = $parsed; 1022 | return $this->bodyParsed; 1023 | } 1024 | 1025 | return null; 1026 | } 1027 | 1028 | /** 1029 | * Return an instance with the specified body parameters. 1030 | * 1031 | * These MAY be injected during instantiation. 1032 | * 1033 | * If the request Content-Type is either application/x-www-form-urlencoded 1034 | * or multipart/form-data, and the request method is POST, use this method 1035 | * ONLY to inject the contents of $_POST. 1036 | * 1037 | * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of 1038 | * deserializing the request body content. Deserialization/parsing returns 1039 | * structured data, and, as such, this method ONLY accepts arrays or objects, 1040 | * or a null value if nothing was available to parse. 1041 | * 1042 | * As an example, if content negotiation determines that the request data 1043 | * is a JSON payload, this method could be used to create a request 1044 | * instance with the deserialized parameters. 1045 | * 1046 | * This method MUST be implemented in such a way as to retain the 1047 | * immutability of the message, and MUST return an instance that has the 1048 | * updated body parameters. 1049 | * 1050 | * @param null|array|object $data The deserialized body data. This will 1051 | * typically be in an array or object. 1052 | * @return self 1053 | * @throws \InvalidArgumentException if an unsupported argument type is 1054 | * provided. 1055 | */ 1056 | public function withParsedBody($data) 1057 | { 1058 | if (!is_null($data) && !is_object($data) && !is_array($data)) { 1059 | throw new InvalidArgumentException('Parsed body value must be an array, an object, or null'); 1060 | } 1061 | 1062 | $clone = clone $this; 1063 | $clone->bodyParsed = $data; 1064 | 1065 | return $clone; 1066 | } 1067 | 1068 | /** 1069 | * Force Body to be parsed again. 1070 | * 1071 | * Note: This method is not part of the PSR-7 standard. 1072 | * 1073 | * @return self 1074 | */ 1075 | public function reparseBody() 1076 | { 1077 | $this->bodyParsed = false; 1078 | 1079 | return $this; 1080 | } 1081 | 1082 | /** 1083 | * Register media type parser. 1084 | * 1085 | * Note: This method is not part of the PSR-7 standard. 1086 | * 1087 | * @param string $mediaType A Http media type (excluding content-type 1088 | * params). 1089 | * @param callable $callable A callable that returns parsed contents for 1090 | * media type. 1091 | */ 1092 | public function registerMediaTypeParser($mediaType, callable $callable) 1093 | { 1094 | if ($callable instanceof Closure) { 1095 | $callable = $callable->bindTo($this); 1096 | } 1097 | $this->bodyParsers[(string)$mediaType] = $callable; 1098 | } 1099 | 1100 | /******************************************************************************* 1101 | * Parameters (e.g., POST and GET data) 1102 | ******************************************************************************/ 1103 | 1104 | /** 1105 | * Fetch request parameter value from body or query string (in that order). 1106 | * 1107 | * Note: This method is not part of the PSR-7 standard. 1108 | * 1109 | * @param string $key The parameter key. 1110 | * @param string $default The default value. 1111 | * 1112 | * @return mixed The parameter value. 1113 | */ 1114 | public function getParam($key, $default = null) 1115 | { 1116 | $postParams = $this->getParsedBody(); 1117 | $getParams = $this->getQueryParams(); 1118 | $result = $default; 1119 | if (is_array($postParams) && isset($postParams[$key])) { 1120 | $result = $postParams[$key]; 1121 | } elseif (is_object($postParams) && property_exists($postParams, $key)) { 1122 | $result = $postParams->$key; 1123 | } elseif (isset($getParams[$key])) { 1124 | $result = $getParams[$key]; 1125 | } 1126 | 1127 | return $result; 1128 | } 1129 | 1130 | /** 1131 | * Fetch parameter value from request body. 1132 | * 1133 | * Note: This method is not part of the PSR-7 standard. 1134 | * 1135 | * @param string $key 1136 | * @param mixed $default 1137 | * 1138 | * @return mixed 1139 | */ 1140 | public function getParsedBodyParam($key, $default = null) 1141 | { 1142 | $postParams = $this->getParsedBody(); 1143 | $result = $default; 1144 | if (is_array($postParams) && isset($postParams[$key])) { 1145 | $result = $postParams[$key]; 1146 | } elseif (is_object($postParams) && property_exists($postParams, $key)) { 1147 | $result = $postParams->$key; 1148 | } 1149 | 1150 | return $result; 1151 | } 1152 | 1153 | /** 1154 | * Fetch parameter value from query string. 1155 | * 1156 | * Note: This method is not part of the PSR-7 standard. 1157 | * 1158 | * @param string $key 1159 | * @param mixed $default 1160 | * 1161 | * @return mixed 1162 | */ 1163 | public function getQueryParam($key, $default = null) 1164 | { 1165 | $getParams = $this->getQueryParams(); 1166 | $result = $default; 1167 | if (isset($getParams[$key])) { 1168 | $result = $getParams[$key]; 1169 | } 1170 | 1171 | return $result; 1172 | } 1173 | 1174 | /** 1175 | * Fetch assocative array of body and query string parameters. 1176 | * 1177 | * Note: This method is not part of the PSR-7 standard. 1178 | * 1179 | * @return array 1180 | */ 1181 | public function getParams() 1182 | { 1183 | $params = $this->getQueryParams(); 1184 | $postParams = $this->getParsedBody(); 1185 | if ($postParams) { 1186 | $params = array_merge($params, (array)$postParams); 1187 | } 1188 | 1189 | return $params; 1190 | } 1191 | } 1192 | -------------------------------------------------------------------------------- /src/Http/RequestBody.php: -------------------------------------------------------------------------------- 1 | 'Continue', 52 | 101 => 'Switching Protocols', 53 | 102 => 'Processing', 54 | //Successful 2xx 55 | 200 => 'OK', 56 | 201 => 'Created', 57 | 202 => 'Accepted', 58 | 203 => 'Non-Authoritative Information', 59 | 204 => 'No Content', 60 | 205 => 'Reset Content', 61 | 206 => 'Partial Content', 62 | 207 => 'Multi-Status', 63 | 208 => 'Already Reported', 64 | 226 => 'IM Used', 65 | //Redirection 3xx 66 | 300 => 'Multiple Choices', 67 | 301 => 'Moved Permanently', 68 | 302 => 'Found', 69 | 303 => 'See Other', 70 | 304 => 'Not Modified', 71 | 305 => 'Use Proxy', 72 | 306 => '(Unused)', 73 | 307 => 'Temporary Redirect', 74 | 308 => 'Permanent Redirect', 75 | //Client Error 4xx 76 | 400 => 'Bad Request', 77 | 401 => 'Unauthorized', 78 | 402 => 'Payment Required', 79 | 403 => 'Forbidden', 80 | 404 => 'Not Found', 81 | 405 => 'Method Not Allowed', 82 | 406 => 'Not Acceptable', 83 | 407 => 'Proxy Authentication Required', 84 | 408 => 'Request Timeout', 85 | 409 => 'Conflict', 86 | 410 => 'Gone', 87 | 411 => 'Length Required', 88 | 412 => 'Precondition Failed', 89 | 413 => 'Request Entity Too Large', 90 | 414 => 'Request-URI Too Long', 91 | 415 => 'Unsupported Media Type', 92 | 416 => 'Requested Range Not Satisfiable', 93 | 417 => 'Expectation Failed', 94 | 418 => 'I\'m a teapot', 95 | 421 => 'Misdirected Request', 96 | 422 => 'Unprocessable Entity', 97 | 423 => 'Locked', 98 | 424 => 'Failed Dependency', 99 | 426 => 'Upgrade Required', 100 | 428 => 'Precondition Required', 101 | 429 => 'Too Many Requests', 102 | 431 => 'Request Header Fields Too Large', 103 | 444 => 'Connection Closed Without Response', 104 | 451 => 'Unavailable For Legal Reasons', 105 | 499 => 'Client Closed Request', 106 | //Server Error 5xx 107 | 500 => 'Internal Server Error', 108 | 501 => 'Not Implemented', 109 | 502 => 'Bad Gateway', 110 | 503 => 'Service Unavailable', 111 | 504 => 'Gateway Timeout', 112 | 505 => 'Http Version Not Supported', 113 | 506 => 'Variant Also Negotiates', 114 | 507 => 'Insufficient Storage', 115 | 508 => 'Loop Detected', 116 | 510 => 'Not Extended', 117 | 511 => 'Network Authentication Required', 118 | 599 => 'Network Connect Timeout Error', 119 | ]; 120 | 121 | /** 122 | * Create new Http response. 123 | * 124 | * @param int $status The response status code. 125 | * @param HeadersInterface|null $headers The response headers. 126 | * @param StreamInterface|null $body The response body. 127 | */ 128 | public function __construct($status = 200, HeadersInterface $headers = null, StreamInterface $body = null) 129 | { 130 | $this->status = $this->filterStatus($status); 131 | $this->headers = $headers ? $headers : new Headers(); 132 | $this->body = $body ? $body : new Body(fopen('php://temp', 'r+')); 133 | } 134 | 135 | /** 136 | * This method is applied to the cloned object 137 | * after PHP performs an initial shallow-copy. This 138 | * method completes a deep-copy by creating new objects 139 | * for the cloned object's internal reference pointers. 140 | */ 141 | public function __clone() 142 | { 143 | $this->headers = clone $this->headers; 144 | } 145 | 146 | /******************************************************************************* 147 | * Status 148 | ******************************************************************************/ 149 | 150 | /** 151 | * Gets the response status code. 152 | * 153 | * The status code is a 3-digit integer result code of the server's attempt 154 | * to understand and satisfy the request. 155 | * 156 | * @return int Status code. 157 | */ 158 | public function getStatusCode() 159 | { 160 | return $this->status; 161 | } 162 | 163 | /** 164 | * Return an instance with the specified status code and, optionally, reason phrase. 165 | * 166 | * If no reason phrase is specified, implementations MAY choose to default 167 | * to the RFC 7231 or IANA recommended reason phrase for the response's 168 | * status code. 169 | * 170 | * This method MUST be implemented in such a way as to retain the 171 | * immutability of the message, and MUST return an instance that has the 172 | * updated status and reason phrase. 173 | * 174 | * @link http://tools.ietf.org/html/rfc7231#section-6 175 | * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml 176 | * @param int $code The 3-digit integer result code to set. 177 | * @param string $reasonPhrase The reason phrase to use with the 178 | * provided status code; if none is provided, implementations MAY 179 | * use the defaults as suggested in the Http specification. 180 | * @return self 181 | * @throws \InvalidArgumentException For invalid status code arguments. 182 | */ 183 | public function withStatus($code, $reasonPhrase = '') 184 | { 185 | $code = $this->filterStatus($code); 186 | 187 | if (!is_string($reasonPhrase) && !method_exists($reasonPhrase, '__toString')) { 188 | throw new InvalidArgumentException('ReasonPhrase must be a string'); 189 | } 190 | 191 | $clone = clone $this; 192 | $clone->status = $code; 193 | if ($reasonPhrase === '' && isset(static::$messages[$code])) { 194 | $reasonPhrase = static::$messages[$code]; 195 | } 196 | 197 | if ($reasonPhrase === '') { 198 | throw new InvalidArgumentException('ReasonPhrase must be supplied for this code'); 199 | } 200 | 201 | $clone->reasonPhrase = $reasonPhrase; 202 | 203 | return $clone; 204 | } 205 | 206 | /** 207 | * Filter Http status code. 208 | * 209 | * @param int $status Http status code. 210 | * @return int 211 | * @throws \InvalidArgumentException If an invalid Http status code is provided. 212 | */ 213 | protected function filterStatus($status) 214 | { 215 | if (!is_integer($status) || $status<100 || $status>599) { 216 | throw new InvalidArgumentException('Invalid Http status code'); 217 | } 218 | 219 | return $status; 220 | } 221 | 222 | /** 223 | * Gets the response reason phrase associated with the status code. 224 | * 225 | * Because a reason phrase is not a required element in a response 226 | * status line, the reason phrase value MAY be null. Implementations MAY 227 | * choose to return the default RFC 7231 recommended reason phrase (or those 228 | * listed in the IANA Http Status Code Registry) for the response's 229 | * status code. 230 | * 231 | * @link http://tools.ietf.org/html/rfc7231#section-6 232 | * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml 233 | * @return string Reason phrase; must return an empty string if none present. 234 | */ 235 | public function getReasonPhrase() 236 | { 237 | if ($this->reasonPhrase) { 238 | return $this->reasonPhrase; 239 | } 240 | if (isset(static::$messages[$this->status])) { 241 | return static::$messages[$this->status]; 242 | } 243 | return ''; 244 | } 245 | 246 | /******************************************************************************* 247 | * Body 248 | ******************************************************************************/ 249 | 250 | /** 251 | * Write data to the response body. 252 | * 253 | * Note: This method is not part of the PSR-7 standard. 254 | * 255 | * Proxies to the underlying stream and writes the provided data to it. 256 | * 257 | * @param string $data 258 | * @return self 259 | */ 260 | public function write($data) 261 | { 262 | $this->getBody()->write($data); 263 | 264 | return $this; 265 | } 266 | 267 | /******************************************************************************* 268 | * Response Helpers 269 | ******************************************************************************/ 270 | 271 | /** 272 | * Redirect. 273 | * 274 | * Note: This method is not part of the PSR-7 standard. 275 | * 276 | * This method prepares the response object to return an Http Redirect 277 | * response to the client. 278 | * 279 | * @param string|UriInterface $url The redirect destination. 280 | * @param int|null $status The redirect Http status code. 281 | * @return self 282 | */ 283 | public function withRedirect($url, $status = null) 284 | { 285 | $responseWithRedirect = $this->withHeader('Location', (string)$url); 286 | 287 | if (is_null($status) && $this->getStatusCode() === 200) { 288 | $status = 302; 289 | } 290 | 291 | if (!is_null($status)) { 292 | return $responseWithRedirect->withStatus($status); 293 | } 294 | 295 | return $responseWithRedirect; 296 | } 297 | 298 | /** 299 | * Json. 300 | * 301 | * Note: This method is not part of the PSR-7 standard. 302 | * 303 | * This method prepares the response object to return an Http Json 304 | * response to the client. 305 | * 306 | * @param mixed $data The data 307 | * @param int $status The Http status code. 308 | * @param int $encodingOptions Json encoding options 309 | * @throws \RuntimeException 310 | * @return self 311 | */ 312 | public function withJson($data, $status = null, $encodingOptions = 0) 313 | { 314 | $response = $this->withBody(new Body(fopen('php://temp', 'r+'))); 315 | $response->body->write($json = json_encode($data, $encodingOptions)); 316 | 317 | // Ensure that the json encoding passed successfully 318 | if ($json === false) { 319 | throw new \RuntimeException(json_last_error_msg(), json_last_error()); 320 | } 321 | 322 | $responseWithJson = $response->withHeader('Content-Type', 'application/json;charset=utf-8'); 323 | if (isset($status)) { 324 | return $responseWithJson->withStatus($status); 325 | } 326 | return $responseWithJson; 327 | } 328 | 329 | /** 330 | * Is this response empty? 331 | * 332 | * Note: This method is not part of the PSR-7 standard. 333 | * 334 | * @return bool 335 | */ 336 | public function isEmpty() 337 | { 338 | return in_array($this->getStatusCode(), [204, 205, 304]); 339 | } 340 | 341 | /** 342 | * Is this response informational? 343 | * 344 | * Note: This method is not part of the PSR-7 standard. 345 | * 346 | * @return bool 347 | */ 348 | public function isInformational() 349 | { 350 | return $this->getStatusCode() >= 100 && $this->getStatusCode() < 200; 351 | } 352 | 353 | /** 354 | * Is this response OK? 355 | * 356 | * Note: This method is not part of the PSR-7 standard. 357 | * 358 | * @return bool 359 | */ 360 | public function isOk() 361 | { 362 | return $this->getStatusCode() === 200; 363 | } 364 | 365 | /** 366 | * Is this response successful? 367 | * 368 | * Note: This method is not part of the PSR-7 standard. 369 | * 370 | * @return bool 371 | */ 372 | public function isSuccessful() 373 | { 374 | return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300; 375 | } 376 | 377 | /** 378 | * Is this response a redirect? 379 | * 380 | * Note: This method is not part of the PSR-7 standard. 381 | * 382 | * @return bool 383 | */ 384 | public function isRedirect() 385 | { 386 | return in_array($this->getStatusCode(), [301, 302, 303, 307]); 387 | } 388 | 389 | /** 390 | * Is this response a redirection? 391 | * 392 | * Note: This method is not part of the PSR-7 standard. 393 | * 394 | * @return bool 395 | */ 396 | public function isRedirection() 397 | { 398 | return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400; 399 | } 400 | 401 | /** 402 | * Is this response forbidden? 403 | * 404 | * Note: This method is not part of the PSR-7 standard. 405 | * 406 | * @return bool 407 | * @api 408 | */ 409 | public function isForbidden() 410 | { 411 | return $this->getStatusCode() === 403; 412 | } 413 | 414 | /** 415 | * Is this response not Found? 416 | * 417 | * Note: This method is not part of the PSR-7 standard. 418 | * 419 | * @return bool 420 | */ 421 | public function isNotFound() 422 | { 423 | return $this->getStatusCode() === 404; 424 | } 425 | 426 | /** 427 | * Is this response a client error? 428 | * 429 | * Note: This method is not part of the PSR-7 standard. 430 | * 431 | * @return bool 432 | */ 433 | public function isClientError() 434 | { 435 | return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500; 436 | } 437 | 438 | /** 439 | * Is this response a server error? 440 | * 441 | * Note: This method is not part of the PSR-7 standard. 442 | * 443 | * @return bool 444 | */ 445 | public function isServerError() 446 | { 447 | return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600; 448 | } 449 | 450 | /** 451 | * Convert response to string. 452 | * 453 | * Note: This method is not part of the PSR-7 standard. 454 | * 455 | * @return string 456 | */ 457 | public function __toString() 458 | { 459 | $output = sprintf( 460 | 'Http/%s %s %s', 461 | $this->getProtocolVersion(), 462 | $this->getStatusCode(), 463 | $this->getReasonPhrase() 464 | ); 465 | $output .= PHP_EOL; 466 | foreach ($this->getHeaders() as $name => $values) { 467 | $output .= sprintf('%s: %s', $name, $this->getHeaderLine($name)) . PHP_EOL; 468 | } 469 | $output .= PHP_EOL; 470 | $output .= (string)$this->getBody(); 471 | 472 | return $output; 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /src/Http/Stream.php: -------------------------------------------------------------------------------- 1 | ['r', 'r+', 'w+', 'a+', 'x+', 'c+'], 44 | 'writable' => ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'], 45 | ]; 46 | 47 | /** 48 | * The underlying stream resource 49 | * 50 | * 优先的 stream 资源 51 | * 52 | * @var resource 53 | */ 54 | protected $stream; 55 | 56 | /** 57 | * Stream metadata 58 | * 59 | * stream 的元信息 60 | * 61 | * @var array 62 | */ 63 | protected $meta; 64 | 65 | /** 66 | * Is this stream readable? 67 | * 68 | * 表达此 stream 是否可读 69 | * 70 | * @var bool 71 | */ 72 | protected $readable; 73 | 74 | /** 75 | * Is this stream writable? 76 | * 77 | * 表达此 stream 是否可写 78 | * 79 | * @var bool 80 | */ 81 | protected $writable; 82 | 83 | /** 84 | * Is this stream seekable? 85 | * 86 | * 表达此 stream 是否可移动读写指针 87 | * 88 | * @var bool 89 | */ 90 | protected $seekable; 91 | 92 | /** 93 | * The size of the stream if known 94 | * 95 | * 已知的 stream 的数据大小 96 | * 97 | * @var null|int 98 | */ 99 | protected $size; 100 | 101 | /** 102 | * Is this stream a pipe? 103 | * 104 | * 表达这个 stream 是否是一个管道 105 | * 106 | * @var bool 107 | */ 108 | protected $isPipe; 109 | 110 | /** 111 | * Create a new Stream. 112 | * 113 | * 创建一个新的 stream 114 | * 115 | * @param resource $stream A PHP resource handle. 一个 PHP 资源句柄 116 | * 117 | * @throws InvalidArgumentException If argument is not a resource. 118 | */ 119 | public function __construct($stream) 120 | { 121 | $this->attach($stream); 122 | } 123 | 124 | /** 125 | * Get stream metadata as an associative array or retrieve a specific key. 126 | * 127 | * The keys returned are identical to the keys returned from PHP's 128 | * stream_get_meta_data() function. 129 | * 130 | * @link http://php.net/manual/en/function.stream-get-meta-data.php 131 | * 132 | * 获取 stream 的元信息 133 | * 134 | * @param string $key Specific metadata to retrieve. 135 | * 136 | * @return array|mixed|null Returns an associative array if no key is 137 | * provided. Returns a specific key value if a key is provided and the 138 | * value is found, or null if the key is not found. 139 | */ 140 | public function getMetadata($key = null) 141 | { 142 | $this->meta = stream_get_meta_data($this->stream); 143 | if (is_null($key) === true) { 144 | return $this->meta; 145 | } 146 | 147 | return isset($this->meta[$key]) ? $this->meta[$key] : null; 148 | } 149 | 150 | /** 151 | * Is a resource attached to this stream? 152 | * 153 | * 获取这个资源是否和一个 stream 绑定 154 | * 155 | * Note: This method is not part of the PSR-7 standard. 156 | * 157 | * @return bool 158 | */ 159 | protected function isAttached() 160 | { 161 | return is_resource($this->stream); 162 | } 163 | 164 | /** 165 | * Attach new resource to this object. 166 | * 167 | * Note: This method is not part of the PSR-7 standard. 168 | * 169 | * @param resource $newStream A PHP resource handle. 170 | * 171 | * @throws InvalidArgumentException If argument is not a valid PHP resource. 172 | */ 173 | protected function attach($newStream) 174 | { 175 | if (is_resource($newStream) === false) { 176 | throw new InvalidArgumentException(__METHOD__ . ' argument must be a valid PHP resource'); 177 | } 178 | 179 | if ($this->isAttached() === true) { 180 | $this->detach(); 181 | } 182 | 183 | $this->stream = $newStream; 184 | } 185 | 186 | /** 187 | * Separates any underlying resources from the stream. 188 | * 189 | * After the stream has been detached, the stream is in an unusable state. 190 | * 191 | * @return resource|null Underlying PHP stream, if any 192 | */ 193 | public function detach() 194 | { 195 | $oldResource = $this->stream; 196 | $this->stream = null; 197 | $this->meta = null; 198 | $this->readable = null; 199 | $this->writable = null; 200 | $this->seekable = null; 201 | $this->size = null; 202 | $this->isPipe = null; 203 | 204 | return $oldResource; 205 | } 206 | 207 | /** 208 | * Reads all data from the stream into a string, from the beginning to end. 209 | * 210 | * This method MUST attempt to seek to the beginning of the stream before 211 | * reading data and read the stream until the end is reached. 212 | * 213 | * Warning: This could attempt to load a large amount of data into memory. 214 | * 215 | * This method MUST NOT raise an exception in order to conform with PHP's 216 | * string casting operations. 217 | * 218 | * 从头到尾将整个 stream 读取到一个字符串中 219 | * 220 | * 这个方法会尝试着将指针定位到 stream 头部读取到尾部 221 | * 有可能会导致巨大的内存占用 222 | * 根据 php 的魔术方法 __tostring 的官方文档, 这个方法不能在运行中抛出任何异常 (否则将导致 fatal error) 223 | * 224 | * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring 225 | * @return string 226 | */ 227 | public function __toString() 228 | { 229 | if (!$this->isAttached()) { 230 | return ''; 231 | } 232 | 233 | try { 234 | $this->rewind(); 235 | return $this->getContents(); 236 | } catch (RuntimeException $e) { 237 | return ''; 238 | } 239 | } 240 | 241 | /** 242 | * Closes the stream and any underlying resources. 243 | * 244 | * 关闭 stream 245 | */ 246 | public function close() 247 | { 248 | if ($this->isAttached() === true) { 249 | if ($this->isPipe()) { 250 | pclose($this->stream); 251 | } else { 252 | fclose($this->stream); 253 | } 254 | } 255 | 256 | $this->detach(); 257 | } 258 | 259 | /** 260 | * Get the size of the stream if known. 261 | * 262 | * 如果可以, 获得 stream 的大小, 用字节表达 263 | * 264 | * @return int|null Returns the size in bytes if known, or null if unknown. 265 | */ 266 | public function getSize() 267 | { 268 | if (!$this->size && $this->isAttached() === true) { 269 | $stats = fstat($this->stream); 270 | $this->size = isset($stats['size']) && !$this->isPipe() ? $stats['size'] : null; 271 | } 272 | 273 | return $this->size; 274 | } 275 | 276 | /** 277 | * Returns the current position of the file read/write pointer 278 | * 279 | * 获取当前的指针 280 | * 281 | * @return int Position of the file pointer 282 | * 283 | * @throws RuntimeException on error. 284 | */ 285 | public function tell() 286 | { 287 | if (!$this->isAttached() || ($position = ftell($this->stream)) === false || $this->isPipe()) { 288 | throw new RuntimeException('Could not get the position of the pointer in stream'); 289 | } 290 | 291 | return $position; 292 | } 293 | 294 | /** 295 | * Returns true if the stream is at the end of the stream. 296 | * 297 | * 获取 stream 是否已经读到 eof 298 | * 299 | * @return bool 300 | */ 301 | public function eof() 302 | { 303 | return $this->isAttached() ? feof($this->stream) : true; 304 | } 305 | 306 | /** 307 | * Returns whether or not the stream is readable. 308 | * 309 | * 获取 stream 是否可读 310 | * 311 | * @return bool 312 | */ 313 | public function isReadable() 314 | { 315 | if ($this->readable === null) { 316 | if ($this->isPipe()) { 317 | $this->readable = true; 318 | } else { 319 | $this->readable = false; 320 | if ($this->isAttached()) { 321 | $meta = $this->getMetadata(); 322 | foreach (self::$modes['readable'] as $mode) { 323 | if (strpos($meta['mode'], $mode) === 0) { 324 | $this->readable = true; 325 | break; 326 | } 327 | } 328 | } 329 | } 330 | } 331 | 332 | return $this->readable; 333 | } 334 | 335 | /** 336 | * Returns whether or not the stream is writable. 337 | * 338 | * 获取 stream 是否可写 339 | * 340 | * @return bool 341 | */ 342 | public function isWritable() 343 | { 344 | if ($this->writable === null) { 345 | $this->writable = false; 346 | if ($this->isAttached()) { 347 | $meta = $this->getMetadata(); 348 | foreach (self::$modes['writable'] as $mode) { 349 | if (strpos($meta['mode'], $mode) === 0) { 350 | $this->writable = true; 351 | break; 352 | } 353 | } 354 | } 355 | } 356 | 357 | return $this->writable; 358 | } 359 | 360 | /** 361 | * Returns whether or not the stream is seekable. 362 | * 363 | * 获取 stream 是否可移动指针 364 | * 365 | * @return bool 366 | */ 367 | public function isSeekable() 368 | { 369 | if ($this->seekable === null) { 370 | $this->seekable = false; 371 | if ($this->isAttached()) { 372 | $meta = $this->getMetadata(); 373 | $this->seekable = !$this->isPipe() && $meta['seekable']; 374 | } 375 | } 376 | 377 | return $this->seekable; 378 | } 379 | 380 | /** 381 | * Seek to a position in the stream. 382 | * 383 | * 直接在 stream 中移动读写指针 384 | * 385 | * @link http://www.php.net/manual/en/function.fseek.php 386 | * 387 | * @param int $offset Stream offset 388 | * @param int $whence Specifies how the cursor position will be calculated 389 | * based on the seek offset. Valid values are identical to the built-in 390 | * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to 391 | * offset bytes SEEK_CUR: Set position to current location plus offset 392 | * SEEK_END: Set position to end-of-stream plus offset. 393 | * 394 | * @throws RuntimeException on failure. 395 | */ 396 | public function seek($offset, $whence = SEEK_SET) 397 | { 398 | // Note that fseek returns 0 on success! 399 | if (!$this->isSeekable() || fseek($this->stream, $offset, $whence) === -1) { 400 | throw new RuntimeException('Could not seek in stream'); 401 | } 402 | } 403 | 404 | /** 405 | * Seek to the beginning of the stream. 406 | * 407 | * If the stream is not seekable, this method will raise an exception; 408 | * otherwise, it will perform a seek(0). 409 | * 410 | * 将 stream 指针移动到头部, 如果 stream 不可以移动指针, 则会抛出异常 411 | * 412 | * @see seek() 413 | * 414 | * @link http://www.php.net/manual/en/function.fseek.php 415 | * 416 | * @throws RuntimeException on failure. 417 | */ 418 | public function rewind() 419 | { 420 | if (!$this->isSeekable() || rewind($this->stream) === false) { 421 | throw new RuntimeException('Could not rewind stream'); 422 | } 423 | } 424 | 425 | /** 426 | * Read data from the stream. 427 | * 428 | * 从 stream 中读取数据 429 | * 430 | * @param int $length Read up to $length bytes from the object and return 431 | * them. Fewer than $length bytes may be returned if underlying stream 432 | * call returns fewer bytes. 433 | * 434 | * @return string Returns the data read from the stream, or an empty string 435 | * if no bytes are available. 436 | * 437 | * @throws RuntimeException if an error occurs. 438 | */ 439 | public function read($length) 440 | { 441 | if (!$this->isReadable() || ($data = fread($this->stream, $length)) === false) { 442 | throw new RuntimeException('Could not read from stream'); 443 | } 444 | 445 | return $data; 446 | } 447 | 448 | /** 449 | * Write data to the stream. 450 | * 451 | * 写入数据到 stream 452 | * 453 | * @param string $string The string that is to be written. 454 | * 455 | * @return int Returns the number of bytes written to the stream. 456 | * 457 | * @throws RuntimeException on failure. 458 | */ 459 | public function write($string) 460 | { 461 | if (!$this->isWritable() || ($written = fwrite($this->stream, $string)) === false) { 462 | throw new RuntimeException('Could not write to stream'); 463 | } 464 | 465 | // reset size so that it will be recalculated on next call to getSize() 466 | $this->size = null; 467 | 468 | return $written; 469 | } 470 | 471 | /** 472 | * Returns the remaining contents in a string 473 | * 474 | * 返回 stream 未读部分内容的字符串 475 | * 476 | * @return string 477 | * 478 | * @throws RuntimeException if unable to read or an error occurs while 479 | * reading. 480 | */ 481 | public function getContents() 482 | { 483 | if (!$this->isReadable() || ($contents = stream_get_contents($this->stream)) === false) { 484 | throw new RuntimeException('Could not get contents of stream'); 485 | } 486 | 487 | return $contents; 488 | } 489 | 490 | /** 491 | * Returns whether or not the stream is a pipe. 492 | * 493 | * 获取某个 stream 是否是一个管道 494 | * 495 | * @return bool 496 | */ 497 | public function isPipe() 498 | { 499 | if ($this->isPipe === null) { 500 | $this->isPipe = false; 501 | if ($this->isAttached()) { 502 | $mode = fstat($this->stream)['mode']; 503 | $this->isPipe = ($mode & self::FSTAT_MODE_S_IFIFO) !== 0; 504 | } 505 | } 506 | 507 | return $this->isPipe; 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /src/Http/UploadedFile.php: -------------------------------------------------------------------------------- 1 | has('slim.files')) { 88 | return $env['slim.files']; 89 | } elseif (isset($_FILES)) { 90 | return static::parseUploadedFiles($_FILES); 91 | } 92 | 93 | return []; 94 | } 95 | 96 | /** 97 | * Parse a non-normalized, i.e. $_FILES superglobal, tree of uploaded file data. 98 | * 99 | * @param array $uploadedFiles The non-normalized tree of uploaded file data. 100 | * 101 | * @return array A normalized tree of UploadedFile instances. 102 | */ 103 | private static function parseUploadedFiles(array $uploadedFiles) 104 | { 105 | $parsed = []; 106 | foreach ($uploadedFiles as $field => $uploadedFile) { 107 | if (!isset($uploadedFile['error'])) { 108 | if (is_array($uploadedFile)) { 109 | $parsed[$field] = static::parseUploadedFiles($uploadedFile); 110 | } 111 | continue; 112 | } 113 | 114 | $parsed[$field] = []; 115 | if (!is_array($uploadedFile['error'])) { 116 | $parsed[$field] = new static( 117 | $uploadedFile['tmp_name'], 118 | isset($uploadedFile['name']) ? $uploadedFile['name'] : null, 119 | isset($uploadedFile['type']) ? $uploadedFile['type'] : null, 120 | isset($uploadedFile['size']) ? $uploadedFile['size'] : null, 121 | $uploadedFile['error'], 122 | true 123 | ); 124 | } else { 125 | $subArray = []; 126 | foreach ($uploadedFile['error'] as $fileIdx => $error) { 127 | // normalise subarray and re-parse to move the input's keyname up a level 128 | $subArray[$fileIdx]['name'] = $uploadedFile['name'][$fileIdx]; 129 | $subArray[$fileIdx]['type'] = $uploadedFile['type'][$fileIdx]; 130 | $subArray[$fileIdx]['tmp_name'] = $uploadedFile['tmp_name'][$fileIdx]; 131 | $subArray[$fileIdx]['error'] = $uploadedFile['error'][$fileIdx]; 132 | $subArray[$fileIdx]['size'] = $uploadedFile['size'][$fileIdx]; 133 | 134 | $parsed[$field] = static::parseUploadedFiles($subArray); 135 | } 136 | } 137 | } 138 | 139 | return $parsed; 140 | } 141 | 142 | /** 143 | * Construct a new UploadedFile instance. 144 | * 145 | * @param string $file The full path to the uploaded file provided by the client. 146 | * @param string|null $name The file name. 147 | * @param string|null $type The file media type. 148 | * @param int|null $size The file size in bytes. 149 | * @param int $error The UPLOAD_ERR_XXX code representing the status of the upload. 150 | * @param bool $sapi Indicates if the upload is in a SAPI environment. 151 | */ 152 | public function __construct($file, $name = null, $type = null, $size = null, $error = UPLOAD_ERR_OK, $sapi = false) 153 | { 154 | $this->file = $file; 155 | $this->name = $name; 156 | $this->type = $type; 157 | $this->size = $size; 158 | $this->error = $error; 159 | $this->sapi = $sapi; 160 | } 161 | 162 | /** 163 | * Retrieve a stream representing the uploaded file. 164 | * 165 | * This method MUST return a StreamInterface instance, representing the 166 | * uploaded file. The purpose of this method is to allow utilizing native PHP 167 | * stream functionality to manipulate the file upload, such as 168 | * stream_copy_to_stream() (though the result will need to be decorated in a 169 | * native PHP stream wrapper to work with such functions). 170 | * 171 | * If the moveTo() method has been called previously, this method MUST raise 172 | * an exception. 173 | * 174 | * @return StreamInterface Stream representation of the uploaded file. 175 | * @throws \RuntimeException in cases when no stream is available or can be 176 | * created. 177 | */ 178 | public function getStream() 179 | { 180 | if ($this->moved) { 181 | throw new \RuntimeException(sprintf('Uploaded file %1s has already been moved', $this->name)); 182 | } 183 | if ($this->stream === null) { 184 | $this->stream = new Stream(fopen($this->file, 'r')); 185 | } 186 | 187 | return $this->stream; 188 | } 189 | 190 | /** 191 | * Move the uploaded file to a new location. 192 | * 193 | * Use this method as an alternative to move_uploaded_file(). This method is 194 | * guaranteed to work in both SAPI and non-SAPI environments. 195 | * Implementations must determine which environment they are in, and use the 196 | * appropriate method (move_uploaded_file(), rename(), or a stream 197 | * operation) to perform the operation. 198 | * 199 | * $targetPath may be an absolute path, or a relative path. If it is a 200 | * relative path, resolution should be the same as used by PHP's rename() 201 | * function. 202 | * 203 | * The original file or stream MUST be removed on completion. 204 | * 205 | * If this method is called more than once, any subsequent calls MUST raise 206 | * an exception. 207 | * 208 | * When used in an SAPI environment where $_FILES is populated, when writing 209 | * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be 210 | * used to ensure permissions and upload status are verified correctly. 211 | * 212 | * If you wish to move to a stream, use getStream(), as SAPI operations 213 | * cannot guarantee writing to stream destinations. 214 | * 215 | * @see http://php.net/is_uploaded_file 216 | * @see http://php.net/move_uploaded_file 217 | * 218 | * @param string $targetPath Path to which to move the uploaded file. 219 | * 220 | * @throws InvalidArgumentException if the $path specified is invalid. 221 | * @throws RuntimeException on any error during the move operation, or on 222 | * the second or subsequent call to the method. 223 | */ 224 | public function moveTo($targetPath) 225 | { 226 | if ($this->moved) { 227 | throw new RuntimeException('Uploaded file already moved'); 228 | } 229 | 230 | $targetIsStream = strpos($targetPath, '://') > 0; 231 | if (!$targetIsStream && !is_writable(dirname($targetPath))) { 232 | throw new InvalidArgumentException('Upload target path is not writable'); 233 | } 234 | 235 | if ($targetIsStream) { 236 | if (!copy($this->file, $targetPath)) { 237 | throw new RuntimeException(sprintf('Error moving uploaded file %1s to %2s', $this->name, $targetPath)); 238 | } 239 | if (!unlink($this->file)) { 240 | throw new RuntimeException(sprintf('Error removing uploaded file %1s', $this->name)); 241 | } 242 | } elseif ($this->sapi) { 243 | if (!is_uploaded_file($this->file)) { 244 | throw new RuntimeException(sprintf('%1s is not a valid uploaded file', $this->file)); 245 | } 246 | 247 | if (!move_uploaded_file($this->file, $targetPath)) { 248 | throw new RuntimeException(sprintf('Error moving uploaded file %1s to %2s', $this->name, $targetPath)); 249 | } 250 | } else { 251 | if (!rename($this->file, $targetPath)) { 252 | throw new RuntimeException(sprintf('Error moving uploaded file %1s to %2s', $this->name, $targetPath)); 253 | } 254 | } 255 | 256 | $this->moved = true; 257 | } 258 | 259 | /** 260 | * Retrieve the error associated with the uploaded file. 261 | * 262 | * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants. 263 | * 264 | * If the file was uploaded successfully, this method MUST return 265 | * UPLOAD_ERR_OK. 266 | * 267 | * Implementations SHOULD return the value stored in the "error" key of 268 | * the file in the $_FILES array. 269 | * 270 | * @see http://php.net/manual/en/features.file-upload.errors.php 271 | * 272 | * @return int One of PHP's UPLOAD_ERR_XXX constants. 273 | */ 274 | public function getError() 275 | { 276 | return $this->error; 277 | } 278 | 279 | /** 280 | * Retrieve the filename sent by the client. 281 | * 282 | * Do not trust the value returned by this method. A client could send 283 | * a malicious filename with the intention to corrupt or hack your 284 | * application. 285 | * 286 | * Implementations SHOULD return the value stored in the "name" key of 287 | * the file in the $_FILES array. 288 | * 289 | * @return string|null The filename sent by the client or null if none 290 | * was provided. 291 | */ 292 | public function getClientFilename() 293 | { 294 | return $this->name; 295 | } 296 | 297 | /** 298 | * Retrieve the media type sent by the client. 299 | * 300 | * Do not trust the value returned by this method. A client could send 301 | * a malicious media type with the intention to corrupt or hack your 302 | * application. 303 | * 304 | * Implementations SHOULD return the value stored in the "type" key of 305 | * the file in the $_FILES array. 306 | * 307 | * @return string|null The media type sent by the client or null if none 308 | * was provided. 309 | */ 310 | public function getClientMediaType() 311 | { 312 | return $this->type; 313 | } 314 | 315 | /** 316 | * Retrieve the file size. 317 | * 318 | * Implementations SHOULD return the value stored in the "size" key of 319 | * the file in the $_FILES array if available, as PHP calculates this based 320 | * on the actual size transmitted. 321 | * 322 | * @return int|null The file size in bytes or null if unknown. 323 | */ 324 | public function getSize() 325 | { 326 | return $this->size; 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /src/Http/Uri.php: -------------------------------------------------------------------------------- 1 | scheme = $this->filterScheme($scheme); 123 | $this->host = $host; 124 | $this->port = $this->filterPort($port); 125 | $this->path = empty($path) ? '/' : $this->filterPath($path); 126 | $this->query = $this->filterQuery($query); 127 | $this->fragment = $this->filterQuery($fragment); 128 | $this->user = $user; 129 | $this->password = $password; 130 | } 131 | 132 | /** 133 | * Create new Uri from string. 134 | * 135 | * @param string $uri Complete Uri string 136 | * (i.e., https://user:pass@host:443/path?query). 137 | * 138 | * @return self 139 | */ 140 | public static function createFromString($uri) 141 | { 142 | if (!is_string($uri) && !method_exists($uri, '__toString')) { 143 | throw new InvalidArgumentException('Uri must be a string'); 144 | } 145 | 146 | $parts = parse_url($uri); 147 | $scheme = isset($parts['scheme']) ? $parts['scheme'] : ''; 148 | $user = isset($parts['user']) ? $parts['user'] : ''; 149 | $pass = isset($parts['pass']) ? $parts['pass'] : ''; 150 | $host = isset($parts['host']) ? $parts['host'] : ''; 151 | $port = isset($parts['port']) ? $parts['port'] : null; 152 | $path = isset($parts['path']) ? $parts['path'] : ''; 153 | $query = isset($parts['query']) ? $parts['query'] : ''; 154 | $fragment = isset($parts['fragment']) ? $parts['fragment'] : ''; 155 | 156 | return new static($scheme, $host, $port, $path, $query, $fragment, $user, $pass); 157 | } 158 | 159 | /** 160 | * Create new Uri from environment. 161 | * 162 | * @param Environment $env 163 | * 164 | * @return self 165 | */ 166 | public static function createFromEnvironment(Environment $env) 167 | { 168 | // Scheme 169 | $isSecure = $env->get('HTTPS'); 170 | $scheme = (empty($isSecure) || $isSecure === 'off') ? 'http' : 'https'; 171 | 172 | // Authority: Username and password 173 | $username = $env->get('PHP_AUTH_USER', ''); 174 | $password = $env->get('PHP_AUTH_PW', ''); 175 | 176 | // Authority: Host 177 | if ($env->has('HTTP_HOST')) { 178 | $host = $env->get('HTTP_HOST'); 179 | } else { 180 | $host = $env->get('SERVER_NAME'); 181 | } 182 | 183 | // Authority: Port 184 | $port = (int)$env->get('SERVER_PORT', 80); 185 | if (preg_match('/^(\[[a-fA-F0-9:.]+\])(:\d+)?\z/', $host, $matches)) { 186 | $host = $matches[1]; 187 | 188 | if ($matches[2]) { 189 | $port = (int) substr($matches[2], 1); 190 | } 191 | } else { 192 | $pos = strpos($host, ':'); 193 | if ($pos !== false) { 194 | $port = (int) substr($host, $pos + 1); 195 | $host = strstr($host, ':', true); 196 | } 197 | } 198 | 199 | // Path 200 | $requestScriptName = parse_url($env->get('SCRIPT_NAME'), PHP_URL_PATH); 201 | $requestScriptDir = dirname($requestScriptName); 202 | 203 | // parse_url() requires a full URL. As we don't extract the domain name or scheme, 204 | // we use a stand-in. 205 | $requestUri = parse_url('http://example.com' . $env->get('REQUEST_URI'), PHP_URL_PATH); 206 | 207 | $basePath = ''; 208 | $virtualPath = $requestUri; 209 | if (stripos($requestUri, $requestScriptName) === 0) { 210 | $basePath = $requestScriptName; 211 | } elseif ($requestScriptDir !== '/' && stripos($requestUri, $requestScriptDir) === 0) { 212 | $basePath = $requestScriptDir; 213 | } 214 | 215 | if ($basePath) { 216 | $virtualPath = ltrim(substr($requestUri, strlen($basePath)), '/'); 217 | } 218 | 219 | // Query string 220 | $queryString = $env->get('QUERY_STRING', ''); 221 | if ($queryString === '') { 222 | $queryString = parse_url('http://example.com' . $env->get('REQUEST_URI'), PHP_URL_QUERY); 223 | } 224 | 225 | // Fragment 226 | $fragment = ''; 227 | 228 | // Build Uri 229 | $uri = new static($scheme, $host, $port, $virtualPath, $queryString, $fragment, $username, $password); 230 | if ($basePath) { 231 | $uri = $uri->withBasePath($basePath); 232 | } 233 | 234 | return $uri; 235 | } 236 | 237 | /******************************************************************************** 238 | * Scheme 239 | *******************************************************************************/ 240 | 241 | /** 242 | * Retrieve the scheme component of the URI. 243 | * 244 | * If no scheme is present, this method MUST return an empty string. 245 | * 246 | * The value returned MUST be normalized to lowercase, per RFC 3986 247 | * Section 3.1. 248 | * 249 | * The trailing ":" character is not part of the scheme and MUST NOT be 250 | * added. 251 | * 252 | * @see https://tools.ietf.org/html/rfc3986#section-3.1 253 | * @return string The URI scheme. 254 | */ 255 | public function getScheme() 256 | { 257 | return $this->scheme; 258 | } 259 | 260 | /** 261 | * Return an instance with the specified scheme. 262 | * 263 | * This method MUST retain the state of the current instance, and return 264 | * an instance that contains the specified scheme. 265 | * 266 | * Implementations MUST support the schemes "http" and "https" case 267 | * insensitively, and MAY accommodate other schemes if required. 268 | * 269 | * An empty scheme is equivalent to removing the scheme. 270 | * 271 | * @param string $scheme The scheme to use with the new instance. 272 | * @return self A new instance with the specified scheme. 273 | * @throws \InvalidArgumentException for invalid or unsupported schemes. 274 | */ 275 | public function withScheme($scheme) 276 | { 277 | $scheme = $this->filterScheme($scheme); 278 | $clone = clone $this; 279 | $clone->scheme = $scheme; 280 | 281 | return $clone; 282 | } 283 | 284 | /** 285 | * Filter Uri scheme. 286 | * 287 | * @param string $scheme Raw Uri scheme. 288 | * @return string 289 | * 290 | * @throws InvalidArgumentException If the Uri scheme is not a string. 291 | * @throws InvalidArgumentException If Uri scheme is not "", "https", or "http". 292 | */ 293 | protected function filterScheme($scheme) 294 | { 295 | static $valid = [ 296 | '' => true, 297 | 'https' => true, 298 | 'http' => true, 299 | ]; 300 | 301 | if (!is_string($scheme) && !method_exists($scheme, '__toString')) { 302 | throw new InvalidArgumentException('Uri scheme must be a string'); 303 | } 304 | 305 | $scheme = str_replace('://', '', strtolower((string)$scheme)); 306 | if (!isset($valid[$scheme])) { 307 | throw new InvalidArgumentException('Uri scheme must be one of: "", "https", "http"'); 308 | } 309 | 310 | return $scheme; 311 | } 312 | 313 | /******************************************************************************** 314 | * Authority 315 | *******************************************************************************/ 316 | 317 | /** 318 | * Retrieve the authority component of the URI. 319 | * 320 | * If no authority information is present, this method MUST return an empty 321 | * string. 322 | * 323 | * The authority syntax of the URI is: 324 | * 325 | *
326 |      * [user-info@]host[:port]
327 |      * 
328 | * 329 | * If the port component is not set or is the standard port for the current 330 | * scheme, it SHOULD NOT be included. 331 | * 332 | * @see https://tools.ietf.org/html/rfc3986#section-3.2 333 | * @return string The URI authority, in "[user-info@]host[:port]" format. 334 | */ 335 | public function getAuthority() 336 | { 337 | $userInfo = $this->getUserInfo(); 338 | $host = $this->getHost(); 339 | $port = $this->getPort(); 340 | 341 | return ($userInfo ? $userInfo . '@' : '') . $host . ($port !== null ? ':' . $port : ''); 342 | } 343 | 344 | /** 345 | * Retrieve the user information component of the URI. 346 | * 347 | * If no user information is present, this method MUST return an empty 348 | * string. 349 | * 350 | * If a user is present in the URI, this will return that value; 351 | * additionally, if the password is also present, it will be appended to the 352 | * user value, with a colon (":") separating the values. 353 | * 354 | * The trailing "@" character is not part of the user information and MUST 355 | * NOT be added. 356 | * 357 | * @return string The URI user information, in "username[:password]" format. 358 | */ 359 | public function getUserInfo() 360 | { 361 | return $this->user . ($this->password ? ':' . $this->password : ''); 362 | } 363 | 364 | /** 365 | * Return an instance with the specified user information. 366 | * 367 | * This method MUST retain the state of the current instance, and return 368 | * an instance that contains the specified user information. 369 | * 370 | * Password is optional, but the user information MUST include the 371 | * user; an empty string for the user is equivalent to removing user 372 | * information. 373 | * 374 | * @param string $user The user name to use for authority. 375 | * @param null|string $password The password associated with $user. 376 | * @return self A new instance with the specified user information. 377 | */ 378 | public function withUserInfo($user, $password = null) 379 | { 380 | $clone = clone $this; 381 | $clone->user = $user; 382 | $clone->password = $password ? $password : ''; 383 | 384 | return $clone; 385 | } 386 | 387 | /** 388 | * Retrieve the host component of the URI. 389 | * 390 | * If no host is present, this method MUST return an empty string. 391 | * 392 | * The value returned MUST be normalized to lowercase, per RFC 3986 393 | * Section 3.2.2. 394 | * 395 | * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 396 | * @return string The URI host. 397 | */ 398 | public function getHost() 399 | { 400 | return $this->host; 401 | } 402 | 403 | /** 404 | * Return an instance with the specified host. 405 | * 406 | * This method MUST retain the state of the current instance, and return 407 | * an instance that contains the specified host. 408 | * 409 | * An empty host value is equivalent to removing the host. 410 | * 411 | * @param string $host The hostname to use with the new instance. 412 | * @return self A new instance with the specified host. 413 | * @throws \InvalidArgumentException for invalid hostnames. 414 | */ 415 | public function withHost($host) 416 | { 417 | $clone = clone $this; 418 | $clone->host = $host; 419 | 420 | return $clone; 421 | } 422 | 423 | /** 424 | * Retrieve the port component of the URI. 425 | * 426 | * If a port is present, and it is non-standard for the current scheme, 427 | * this method MUST return it as an integer. If the port is the standard port 428 | * used with the current scheme, this method SHOULD return null. 429 | * 430 | * If no port is present, and no scheme is present, this method MUST return 431 | * a null value. 432 | * 433 | * If no port is present, but a scheme is present, this method MAY return 434 | * the standard port for that scheme, but SHOULD return null. 435 | * 436 | * @return null|int The URI port. 437 | */ 438 | public function getPort() 439 | { 440 | return $this->port && !$this->hasStandardPort() ? $this->port : null; 441 | } 442 | 443 | /** 444 | * Return an instance with the specified port. 445 | * 446 | * This method MUST retain the state of the current instance, and return 447 | * an instance that contains the specified port. 448 | * 449 | * Implementations MUST raise an exception for ports outside the 450 | * established TCP and UDP port ranges. 451 | * 452 | * A null value provided for the port is equivalent to removing the port 453 | * information. 454 | * 455 | * @param null|int $port The port to use with the new instance; a null value 456 | * removes the port information. 457 | * @return self A new instance with the specified port. 458 | * @throws \InvalidArgumentException for invalid ports. 459 | */ 460 | public function withPort($port) 461 | { 462 | $port = $this->filterPort($port); 463 | $clone = clone $this; 464 | $clone->port = $port; 465 | 466 | return $clone; 467 | } 468 | 469 | /** 470 | * Does this Uri use a standard port? 471 | * 472 | * @return bool 473 | */ 474 | protected function hasStandardPort() 475 | { 476 | return ($this->scheme === 'http' && $this->port === 80) || ($this->scheme === 'https' && $this->port === 443); 477 | } 478 | 479 | /** 480 | * Filter Uri port. 481 | * 482 | * @param null|int $port The Uri port number. 483 | * @return null|int 484 | * 485 | * @throws InvalidArgumentException If the port is invalid. 486 | */ 487 | protected function filterPort($port) 488 | { 489 | if (is_null($port) || (is_integer($port) && ($port >= 1 && $port <= 65535))) { 490 | return $port; 491 | } 492 | 493 | throw new InvalidArgumentException('Uri port must be null or an integer between 1 and 65535 (inclusive)'); 494 | } 495 | 496 | /******************************************************************************** 497 | * Path 498 | *******************************************************************************/ 499 | 500 | /** 501 | * Retrieve the path component of the URI. 502 | * 503 | * The path can either be empty or absolute (starting with a slash) or 504 | * rootless (not starting with a slash). Implementations MUST support all 505 | * three syntaxes. 506 | * 507 | * Normally, the empty path "" and absolute path "/" are considered equal as 508 | * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically 509 | * do this normalization because in contexts with a trimmed base path, e.g. 510 | * the front controller, this difference becomes significant. It's the task 511 | * of the user to handle both "" and "/". 512 | * 513 | * The value returned MUST be percent-encoded, but MUST NOT double-encode 514 | * any characters. To determine what characters to encode, please refer to 515 | * RFC 3986, Sections 2 and 3.3. 516 | * 517 | * As an example, if the value should include a slash ("/") not intended as 518 | * delimiter between path segments, that value MUST be passed in encoded 519 | * form (e.g., "%2F") to the instance. 520 | * 521 | * @see https://tools.ietf.org/html/rfc3986#section-2 522 | * @see https://tools.ietf.org/html/rfc3986#section-3.3 523 | * @return string The URI path. 524 | */ 525 | public function getPath() 526 | { 527 | return $this->path; 528 | } 529 | 530 | /** 531 | * Return an instance with the specified path. 532 | * 533 | * This method MUST retain the state of the current instance, and return 534 | * an instance that contains the specified path. 535 | * 536 | * The path can either be empty or absolute (starting with a slash) or 537 | * rootless (not starting with a slash). Implementations MUST support all 538 | * three syntaxes. 539 | * 540 | * If the path is intended to be domain-relative rather than path relative then 541 | * it must begin with a slash ("/"). Paths not starting with a slash ("/") 542 | * are assumed to be relative to some base path known to the application or 543 | * consumer. 544 | * 545 | * Users can provide both encoded and decoded path characters. 546 | * Implementations ensure the correct encoding as outlined in getPath(). 547 | * 548 | * @param string $path The path to use with the new instance. 549 | * @return self A new instance with the specified path. 550 | * @throws \InvalidArgumentException for invalid paths. 551 | */ 552 | public function withPath($path) 553 | { 554 | if (!is_string($path)) { 555 | throw new InvalidArgumentException('Uri path must be a string'); 556 | } 557 | 558 | $clone = clone $this; 559 | $clone->path = $this->filterPath($path); 560 | 561 | // if the path is absolute, then clear basePath 562 | if (substr($path, 0, 1) == '/') { 563 | $clone->basePath = ''; 564 | } 565 | 566 | return $clone; 567 | } 568 | 569 | /** 570 | * Retrieve the base path segment of the URI. 571 | * 572 | * Note: This method is not part of the PSR-7 standard. 573 | * 574 | * This method MUST return a string; if no path is present it MUST return 575 | * an empty string. 576 | * 577 | * @return string The base path segment of the URI. 578 | */ 579 | public function getBasePath() 580 | { 581 | return $this->basePath; 582 | } 583 | 584 | /** 585 | * Set base path. 586 | * 587 | * Note: This method is not part of the PSR-7 standard. 588 | * 589 | * @param string $basePath 590 | * @return self 591 | */ 592 | public function withBasePath($basePath) 593 | { 594 | if (!is_string($basePath)) { 595 | throw new InvalidArgumentException('Uri path must be a string'); 596 | } 597 | if (!empty($basePath)) { 598 | $basePath = '/' . trim($basePath, '/'); // <-- Trim on both sides 599 | } 600 | $clone = clone $this; 601 | 602 | if ($basePath !== '/') { 603 | $clone->basePath = $this->filterPath($basePath); 604 | } 605 | 606 | return $clone; 607 | } 608 | 609 | /** 610 | * Filter Uri path. 611 | * 612 | * This method percent-encodes all reserved 613 | * characters in the provided path string. This method 614 | * will NOT double-encode characters that are already 615 | * percent-encoded. 616 | * 617 | * @param string $path The raw uri path. 618 | * @return string The RFC 3986 percent-encoded uri path. 619 | * @link http://www.faqs.org/rfcs/rfc3986.html 620 | */ 621 | protected function filterPath($path) 622 | { 623 | return preg_replace_callback( 624 | '/(?:[^a-zA-Z0-9_\-\.~:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/', 625 | function ($match) { 626 | return rawurlencode($match[0]); 627 | }, 628 | $path 629 | ); 630 | } 631 | 632 | /******************************************************************************** 633 | * Query 634 | *******************************************************************************/ 635 | 636 | /** 637 | * Retrieve the query string of the URI. 638 | * 639 | * If no query string is present, this method MUST return an empty string. 640 | * 641 | * The leading "?" character is not part of the query and MUST NOT be 642 | * added. 643 | * 644 | * The value returned MUST be percent-encoded, but MUST NOT double-encode 645 | * any characters. To determine what characters to encode, please refer to 646 | * RFC 3986, Sections 2 and 3.4. 647 | * 648 | * As an example, if a value in a key/value pair of the query string should 649 | * include an ampersand ("&") not intended as a delimiter between values, 650 | * that value MUST be passed in encoded form (e.g., "%26") to the instance. 651 | * 652 | * @see https://tools.ietf.org/html/rfc3986#section-2 653 | * @see https://tools.ietf.org/html/rfc3986#section-3.4 654 | * @return string The URI query string. 655 | */ 656 | public function getQuery() 657 | { 658 | return $this->query; 659 | } 660 | 661 | /** 662 | * Return an instance with the specified query string. 663 | * 664 | * This method MUST retain the state of the current instance, and return 665 | * an instance that contains the specified query string. 666 | * 667 | * Users can provide both encoded and decoded query characters. 668 | * Implementations ensure the correct encoding as outlined in getQuery(). 669 | * 670 | * An empty query string value is equivalent to removing the query string. 671 | * 672 | * @param string $query The query string to use with the new instance. 673 | * @return self A new instance with the specified query string. 674 | * @throws \InvalidArgumentException for invalid query strings. 675 | */ 676 | public function withQuery($query) 677 | { 678 | if (!is_string($query) && !method_exists($query, '__toString')) { 679 | throw new InvalidArgumentException('Uri query must be a string'); 680 | } 681 | $query = ltrim((string)$query, '?'); 682 | $clone = clone $this; 683 | $clone->query = $this->filterQuery($query); 684 | 685 | return $clone; 686 | } 687 | 688 | /** 689 | * Filters the query string or fragment of a URI. 690 | * 691 | * @param string $query The raw uri query string. 692 | * @return string The percent-encoded query string. 693 | */ 694 | protected function filterQuery($query) 695 | { 696 | return preg_replace_callback( 697 | '/(?:[^a-zA-Z0-9_\-\.~!\$&\'\(\)\*\+,;=%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/', 698 | function ($match) { 699 | return rawurlencode($match[0]); 700 | }, 701 | $query 702 | ); 703 | } 704 | 705 | /******************************************************************************** 706 | * Fragment 707 | *******************************************************************************/ 708 | 709 | /** 710 | * Retrieve the fragment component of the URI. 711 | * 712 | * If no fragment is present, this method MUST return an empty string. 713 | * 714 | * The leading "#" character is not part of the fragment and MUST NOT be 715 | * added. 716 | * 717 | * The value returned MUST be percent-encoded, but MUST NOT double-encode 718 | * any characters. To determine what characters to encode, please refer to 719 | * RFC 3986, Sections 2 and 3.5. 720 | * 721 | * @see https://tools.ietf.org/html/rfc3986#section-2 722 | * @see https://tools.ietf.org/html/rfc3986#section-3.5 723 | * @return string The URI fragment. 724 | */ 725 | public function getFragment() 726 | { 727 | return $this->fragment; 728 | } 729 | 730 | /** 731 | * Return an instance with the specified URI fragment. 732 | * 733 | * This method MUST retain the state of the current instance, and return 734 | * an instance that contains the specified URI fragment. 735 | * 736 | * Users can provide both encoded and decoded fragment characters. 737 | * Implementations ensure the correct encoding as outlined in getFragment(). 738 | * 739 | * An empty fragment value is equivalent to removing the fragment. 740 | * 741 | * @param string $fragment The fragment to use with the new instance. 742 | * @return self A new instance with the specified fragment. 743 | */ 744 | public function withFragment($fragment) 745 | { 746 | if (!is_string($fragment) && !method_exists($fragment, '__toString')) { 747 | throw new InvalidArgumentException('Uri fragment must be a string'); 748 | } 749 | $fragment = ltrim((string)$fragment, '#'); 750 | $clone = clone $this; 751 | $clone->fragment = $this->filterQuery($fragment); 752 | 753 | return $clone; 754 | } 755 | 756 | /******************************************************************************** 757 | * Helpers 758 | *******************************************************************************/ 759 | 760 | /** 761 | * Return the string representation as a URI reference. 762 | * 763 | * Depending on which components of the URI are present, the resulting 764 | * string is either a full URI or relative reference according to RFC 3986, 765 | * Section 4.1. The method concatenates the various components of the URI, 766 | * using the appropriate delimiters: 767 | * 768 | * - If a scheme is present, it MUST be suffixed by ":". 769 | * - If an authority is present, it MUST be prefixed by "//". 770 | * - The path can be concatenated without delimiters. But there are two 771 | * cases where the path has to be adjusted to make the URI reference 772 | * valid as PHP does not allow to throw an exception in __toString(): 773 | * - If the path is rootless and an authority is present, the path MUST 774 | * be prefixed by "/". 775 | * - If the path is starting with more than one "/" and no authority is 776 | * present, the starting slashes MUST be reduced to one. 777 | * - If a query is present, it MUST be prefixed by "?". 778 | * - If a fragment is present, it MUST be prefixed by "#". 779 | * 780 | * @see http://tools.ietf.org/html/rfc3986#section-4.1 781 | * @return string 782 | */ 783 | public function __toString() 784 | { 785 | $scheme = $this->getScheme(); 786 | $authority = $this->getAuthority(); 787 | $basePath = $this->getBasePath(); 788 | $path = $this->getPath(); 789 | $query = $this->getQuery(); 790 | $fragment = $this->getFragment(); 791 | 792 | $path = $basePath . '/' . ltrim($path, '/'); 793 | 794 | return ($scheme ? $scheme . ':' : '') 795 | . ($authority ? '//' . $authority : '') 796 | . $path 797 | . ($query ? '?' . $query : '') 798 | . ($fragment ? '#' . $fragment : ''); 799 | } 800 | 801 | /** 802 | * Return the fully qualified base URL. 803 | * 804 | * Note that this method never includes a trailing / 805 | * 806 | * This method is not part of PSR-7. 807 | * 808 | * @return string 809 | */ 810 | public function getBaseUrl() 811 | { 812 | $scheme = $this->getScheme(); 813 | $authority = $this->getAuthority(); 814 | $basePath = $this->getBasePath(); 815 | 816 | if ($authority && substr($basePath, 0, 1) !== '/') { 817 | $basePath = $basePath . '/' . $basePath; 818 | } 819 | 820 | return ($scheme ? $scheme . ':' : '') 821 | . ($authority ? '//' . $authority : '') 822 | . rtrim($basePath, '/'); 823 | } 824 | } 825 | -------------------------------------------------------------------------------- /src/Interfaces/ContainerInterface.php: -------------------------------------------------------------------------------- 1 | set( 48 | * (new ElementDefinition()) 49 | * ->setType(MyClass::class) 50 | * ->setBuilderToConstructor() 51 | * ->setDeferred() 52 | * ->setPrototypeScope() 53 | * ); 54 | * 不过一直到类名被getByType访问之前, 都不会被调用 55 | * 56 | * @fixme 严格的讲, 这个不应该属于容器的职责, 大家可以考虑一下如何把这部分逻辑剥离出容器的接口 57 | * 58 | * @param string $namespace 待注册的命名空间 59 | * @return void 60 | * @throws \Smile\Exceptions\ContainerException 61 | */ 62 | public function enableAutowiredForNamespace($namespace); 63 | 64 | } -------------------------------------------------------------------------------- /src/Interfaces/ContainerProviderInterface.php: -------------------------------------------------------------------------------- 1 | name); 67 | } 68 | 69 | /** 70 | * 获取是否设置了方法 71 | * 72 | * @return bool 73 | */ 74 | public function hasMethods() 75 | { 76 | return count($this->methods) > 0; 77 | } 78 | 79 | /** 80 | * 获取是否设置了解析目标 81 | * 82 | * @return bool 83 | */ 84 | public function hasTarget() 85 | { 86 | return isset($this->target); 87 | } 88 | 89 | /** 90 | * 获取名称 91 | * 92 | * @return string 93 | */ 94 | public function getName() 95 | { 96 | return $this->name; 97 | } 98 | 99 | /** 100 | * 获取所有方法 101 | * 102 | * @return array 103 | */ 104 | public function getMethods() 105 | { 106 | return $this->methods; 107 | } 108 | 109 | /** 110 | * 返回 Map 封装后的 URL 部件 111 | * 112 | * @return Map 113 | */ 114 | public function getParts() 115 | { 116 | return new Map($this->parts); 117 | } 118 | 119 | /** 120 | * 获取执行目标 121 | * 122 | * @return mixed 123 | */ 124 | public function getTarget() 125 | { 126 | return $this->target; 127 | } 128 | 129 | /** 130 | * @return string 131 | */ 132 | public function getUrlRule() 133 | { 134 | return $this->urlRule; 135 | } 136 | 137 | /** 138 | * 设置名称 139 | * 140 | * @param string $name 141 | * @return Route 142 | */ 143 | public function setName($name) 144 | { 145 | $this->name = $name; 146 | return $this; 147 | } 148 | 149 | /** 150 | * 设置解析目标 151 | * 152 | * @param mixed $target 153 | * @return Route 154 | */ 155 | public function setTarget($target) 156 | { 157 | $this->target = $target; 158 | return $this; 159 | } 160 | 161 | /** 162 | * 映射方法到 URL 规则 163 | * @param $methods 164 | * @param $urlRule 165 | */ 166 | public function map($methods, $urlRule) 167 | { 168 | $this->assertUrlRule($urlRule); 169 | $this->urlRule = $urlRule; 170 | $this->setupRegexRule(); 171 | 172 | if (is_string($methods)) { 173 | $this->methods = explode('|', $methods); 174 | } elseif (is_array($methods)) { 175 | $this->methods = $methods; 176 | } 177 | 178 | array_walk($this->methods, function (&$method) { 179 | $method = strtolower($method); 180 | if (!in_array($method, self::METHODS)) { 181 | throw new RouteException('不合法的方法'); 182 | } 183 | }); 184 | } 185 | 186 | /** 187 | * 为 URL 规则绑定 Restful 协议的几种方法 188 | * @param $urlRule 189 | * @return $this 190 | */ 191 | public function restful($urlRule) 192 | { 193 | $this->map(self::METHODS, $urlRule); 194 | return $this; 195 | } 196 | 197 | /** 198 | * 为 URL 规则绑定 GET 方法 199 | * @param $urlRule 200 | * @return $this 201 | */ 202 | public function get($urlRule) 203 | { 204 | $this->map([self::METHOD_GET], $urlRule); 205 | return $this; 206 | } 207 | 208 | /** 209 | * 为 URL 规则绑定 POST 方法 210 | * @param $urlRule 211 | * @return $this 212 | */ 213 | public function post($urlRule) 214 | { 215 | $this->map([self::METHOD_POST], $urlRule); 216 | return $this; 217 | } 218 | 219 | /** 220 | * 为 URL 规则绑定 PUT 方法 221 | * @param $urlRule 222 | * @return $this 223 | */ 224 | public function put($urlRule) 225 | { 226 | $this->map([self::METHOD_PUT], $urlRule); 227 | return $this; 228 | } 229 | 230 | /** 231 | * 为 URL 规则绑定 DELETE 方法 232 | * @param $urlRule 233 | * @return $this 234 | */ 235 | public function delete($urlRule) 236 | { 237 | $this->map([self::METHOD_DELETE], $urlRule); 238 | return $this; 239 | } 240 | 241 | /** 242 | * 匹配 URL 是否符合这个路由规则 243 | * @param string $url 待匹配 URL 244 | * @return bool 245 | */ 246 | public function matchUrl($url) 247 | { 248 | $matches = []; 249 | if (preg_match($this->regexUrlPattern, $url, $matches)) { 250 | $this->url = $url; 251 | //删掉全局匹配(这里等价于完整字符串) 252 | array_shift($matches); 253 | $this->parts = $matches; 254 | return true; 255 | } else { 256 | return false; 257 | } 258 | } 259 | 260 | /** 261 | * 根据 parts 生成 URL 262 | * 263 | * @param array $parts 264 | * @return string 265 | */ 266 | public function generatePath(array $parts) 267 | { 268 | $template = str_replace(['[', ']'], ['', ''], $this->urlRule); 269 | $keys = []; 270 | $values = []; 271 | foreach ($parts as $k => $v) { 272 | $keys[] = '{' . $k . '}'; 273 | $values[] = $v; 274 | } 275 | return str_replace($keys, $values, $template); 276 | } 277 | 278 | /** 279 | * 断言 URL 规则合法 280 | * @param $urlRule 281 | * @throws RouteException 282 | */ 283 | private function assertUrlRule($urlRule) 284 | { 285 | if (empty($urlRule) or !is_string($urlRule)) { 286 | throw new RouteException('URL 规则不合法'); 287 | } 288 | } 289 | 290 | /** 291 | * 将 urlRule 的语法翻译成正则表达式 292 | */ 293 | private function setupRegexRule() 294 | { 295 | $urlRule = $this->urlRule; 296 | $regexUrlPattern = str_replace(['[', ']', '/'], ['(?:', ')?', '\/'], $urlRule); 297 | $regexUrlPattern = preg_replace('/\{(\S+?)\}/', '(?<$1>\S+?)', $regexUrlPattern); 298 | $regexUrlPattern = '/^' . $regexUrlPattern . '$/'; 299 | $this->regexUrlPattern = $regexUrlPattern; 300 | } 301 | 302 | 303 | } -------------------------------------------------------------------------------- /src/Router/Router.php: -------------------------------------------------------------------------------- 1 | [ 22 | * Route, Route, Route 23 | * ], 24 | * 'post' => [ 25 | * Route, Route 26 | * ], 27 | * ] 28 | * @var array 29 | */ 30 | private $methodMap = []; 31 | 32 | /** 33 | * 添加一个路由规则 34 | * @param Route $route 35 | * @throws RouterException 36 | */ 37 | public function addRoute(Route $route) 38 | { 39 | if (!$route->hasMethods()) { 40 | throw new RouterException('路由方法未定义'); 41 | } 42 | if (!$route->hasTarget()) { 43 | throw new RouterException('路由目标未定义'); 44 | } 45 | 46 | $methods = $route->getMethods(); 47 | foreach ($methods as $method) { 48 | $this->methodMap[$method][] = $route; 49 | } 50 | 51 | if ($route->hasName()) { 52 | $name = $route->getName(); 53 | if (!empty($this->nameMap[$name])) { 54 | throw new RouterException(sprintf('这个名称的路由规则已经存在: %s', $name)); 55 | } 56 | $this->nameMap[$name] = $route; 57 | } 58 | } 59 | 60 | /** 61 | * 解析一个 URL, 返回路由规则 62 | * 63 | * @param string $method 请求方法 64 | * @param string $url URL 65 | * @return Route 66 | * @throws RouterException 67 | */ 68 | public function resolve($method, $url) 69 | { 70 | $method = strtolower($method); 71 | if (!in_array($method, Route::METHODS)) { 72 | throw new RouterException(sprintf('不支持的请求方法: %s $url', $method, $url)); 73 | } 74 | 75 | /** @var Route[] $routes */ 76 | $routes = $this->methodMap[$method]; 77 | 78 | foreach ($routes as $route) { 79 | if ($route->matchUrl($url)) { 80 | return $route; 81 | } 82 | } 83 | return null; 84 | } 85 | 86 | /** 87 | * 根据路由规则名称和参数返回路径 (可用于 URL 生成) 88 | * 89 | * @param string $name 90 | * @param array|null $parts 91 | * @return string 92 | */ 93 | public function generatePath($name, array $parts = []) 94 | { 95 | $route = $this->getRouteByName($name); 96 | return $route->generatePath($parts); 97 | } 98 | 99 | /** 100 | * 根据名称获得路由规则 101 | * 102 | * @param $name 103 | * @return null|Route 104 | * @throws RouterException 105 | */ 106 | private function getRouteByName($name) 107 | { 108 | if (!empty($this->nameMap[$name])) { 109 | return $this->nameMap[$name]; 110 | } else { 111 | throw new RouterException(sprintf('不存在的路由规则名称: %s', $name)); 112 | } 113 | } 114 | 115 | } -------------------------------------------------------------------------------- /src/Util/Map.php: -------------------------------------------------------------------------------- 1 | data = $data; 16 | } 17 | 18 | public function get($key, $defaultValue = null) 19 | { 20 | if (isset($this->data[$key])) { 21 | return $this->data[$key]; 22 | } else { 23 | return $defaultValue; 24 | } 25 | } 26 | 27 | public function getAll() 28 | { 29 | return $this->data; 30 | } 31 | 32 | public function keys() 33 | { 34 | return array_keys($this->data); 35 | } 36 | 37 | public function has($key) 38 | { 39 | return array_key_exists($key, $this->data); 40 | } 41 | 42 | public function set($key, $value) 43 | { 44 | $this->data[$key] = $value; 45 | } 46 | 47 | public function remove($key) 48 | { 49 | unset($this->data[$key]); 50 | } 51 | 52 | public function clear() 53 | { 54 | $this->data = []; 55 | } 56 | 57 | public function replace(array $data) 58 | { 59 | foreach ($data as $key => $value) 60 | { 61 | $this->data[$key] = $value; 62 | } 63 | } 64 | 65 | public function offsetExists($offset) 66 | { 67 | return $this->has($offset); 68 | } 69 | 70 | public function offsetGet($offset) 71 | { 72 | return $this->get($offset); 73 | } 74 | 75 | public function offsetSet($offset, $value) 76 | { 77 | $this->set($offset, $value); 78 | } 79 | 80 | public function offsetUnset($offset) 81 | { 82 | $this->remove($offset); 83 | } 84 | } -------------------------------------------------------------------------------- /tests/Test/ApplicationTest.php: -------------------------------------------------------------------------------- 1 | enableAutowiredForNamespace(__NAMESPACE__); 25 | }, 26 | function (RouterInterface $router) { 27 | $router->addRoute( 28 | (new Route) 29 | ->put('/{a}[/{b}[/{c}]]') 30 | ->setTarget(TestController::class) 31 | ); 32 | } 33 | ], 34 | [ 35 | function (ContainerInterface $container) { 36 | $container->enableAutowiredForNamespace(__NAMESPACE__); 37 | }, 38 | function (RouterInterface $router) { 39 | $router->addRoute( 40 | (new Route) 41 | ->put('/{a}[/{b}[/{c}]]') 42 | ->setTarget([AnotherTestController::class, 'hello']) 43 | ); 44 | } 45 | ] 46 | ]; 47 | } 48 | 49 | /** 50 | * @dataProvider initializeProvider 51 | * @param callable $containerInit 52 | * @param callable $routerInit 53 | */ 54 | public function testApplication(callable $containerInit, callable $routerInit) 55 | { 56 | $application = new Application($containerInit); 57 | /** @var Environment $environment */ 58 | $environment = $application->getContainer()->getByAlias('environment'); 59 | $environment->replace([ 60 | 'REQUEST_METHOD' => 'PUT', 61 | 'REQUEST_URI' => '/foo/bar/test', 62 | 'QUERY_STRING' => 'abc=123&foo=bar', 63 | 'SERVER_NAME' => 'example.com', 64 | 'CONTENT_TYPE' => 'application/json;charset=utf8', 65 | 'CONTENT_LENGTH' => 15 66 | ]); 67 | $application->loadRouterConfig($routerInit); 68 | $application->run(); 69 | } 70 | 71 | } 72 | 73 | class TestController extends BaseController 74 | { 75 | public function test(ServerRequestInterface $request, ResponseInterface $response) 76 | { 77 | return $response->withJson(['abcdefg']); 78 | } 79 | } 80 | 81 | 82 | class AnotherTestController extends BaseController 83 | { 84 | public function hello(ServerRequestInterface $request, ResponseInterface $response) 85 | { 86 | return $response->withJson(['hello']); 87 | } 88 | } -------------------------------------------------------------------------------- /tests/Test/DI/ContainerTest.php: -------------------------------------------------------------------------------- 1 | enableAutowiredForNamespace(__NAMESPACE__); 36 | $object = $container->getByType(TestClassA::class); 37 | $this->assertInstanceOf(TestClassA::class, $object); 38 | } 39 | 40 | /** 41 | * 测试立即初始化 42 | * 43 | * @dataProvider containerProvider 44 | * @param \Smile\Interfaces\ContainerInterface $container 45 | */ 46 | public function testEagerInit(ContainerInterface $container) 47 | { 48 | $container->set( 49 | (new ElementDefinition()) 50 | ->setType(TestClassB::class) 51 | ->setEager() 52 | ->setSingletonScope() 53 | ); 54 | 55 | $object = $container->getByType(TestClassB::class); 56 | $this->assertInstanceOf(TestClassB::class, $object); 57 | 58 | $this->expectException(\Smile\Exceptions\ContainerException::class); 59 | $this->expectExceptionMessageRegExp('/原型作用域不支持立即实例化/'); 60 | $container->set( 61 | (new ElementDefinition()) 62 | ->setType(TestClassC::class) 63 | ->setEager() 64 | ); 65 | $object = $container->getByType(TestClassC::class); 66 | } 67 | 68 | /** 69 | * 测试延迟初始化 70 | * 71 | * @dataProvider containerProvider 72 | * @param ContainerInterface $container 73 | */ 74 | public function testDeferredInit(\Smile\Interfaces\ContainerInterface $container) 75 | { 76 | $container->set( 77 | (new ElementDefinition()) 78 | ->setType(TestClassB::class) 79 | ->setDeferred() 80 | ); 81 | $object = $container->getByType(TestClassB::class); 82 | $this->assertInstanceOf(TestClassB::class, $object); 83 | } 84 | 85 | /** 86 | * 测试原型作用域 87 | * 88 | * @dataProvider containerProvider 89 | * @param \Smile\Interfaces\ContainerInterface $container 90 | */ 91 | public function testPrototype(ContainerInterface $container) 92 | { 93 | $container->set( 94 | (new ElementDefinition()) 95 | ->setType(TestClassB::class) 96 | ->setDeferred() 97 | ->setPrototypeScope() 98 | ); 99 | 100 | $obj1 = $container->getByType(TestClassB::class); 101 | $obj2 = $container->getByType(TestClassB::class); 102 | 103 | $this->assertNotSame($obj1, $obj2); 104 | } 105 | 106 | /** 107 | * 测试单例作用域 108 | * 109 | * @dataProvider containerProvider 110 | * @param \Smile\Interfaces\ContainerInterface $container 111 | */ 112 | public function testSingleton(\Smile\Interfaces\ContainerInterface $container) 113 | { 114 | $container->set( 115 | (new ElementDefinition()) 116 | ->setType(TestClassB::class) 117 | ->setDeferred() 118 | ->setSingletonScope() 119 | ); 120 | 121 | $obj1 = $container->getByType(TestClassB::class); 122 | $obj2 = $container->getByType(TestClassB::class); 123 | 124 | $this->assertSame($obj1, $obj2); 125 | } 126 | 127 | /** 128 | * 测试循环引用报错 129 | * 130 | * @dataProvider containerProvider 131 | * @param ContainerInterface $container 132 | */ 133 | public function testCircleDep(\Smile\Interfaces\ContainerInterface $container) 134 | { 135 | $this->expectException(\Smile\Exceptions\ContainerException::class); 136 | $this->expectExceptionMessageRegExp('/循环/'); 137 | $container->enableAutowiredForNamespace(__NAMESPACE__); 138 | $obj = $container->getByType(TestCircleDepClassA::class); 139 | } 140 | 141 | /** 142 | * 测试别名 143 | * 144 | * @dataProvider containerProvider 145 | * @param ContainerInterface $container 146 | */ 147 | public function testAlias(\Smile\Interfaces\ContainerInterface $container) 148 | { 149 | $container->set( 150 | (new ElementDefinition()) 151 | ->setType(ElementDefinition::TYPE_ARRAY) 152 | ->setBuilder(function ($hello, $helloCharCount) { 153 | return [$hello, $helloCharCount]; 154 | }) 155 | ->setAlias('helloWorldAndItsLength') 156 | ); 157 | 158 | $container->set( 159 | (new ElementDefinition()) 160 | ->setType(ElementDefinition::TYPE_STRING) 161 | ->setInstance('hello, world') 162 | ->setAlias('hello') 163 | ); 164 | 165 | $container->set( 166 | (new ElementDefinition()) 167 | ->setType(ElementDefinition::TYPE_INT) 168 | ->setBuilder(function ($hello) { 169 | return strlen($hello); 170 | }) 171 | ->setAlias('helloCharCount') 172 | ); 173 | 174 | $arrayResult = $container->getByAlias('helloWorldAndItsLength'); 175 | 176 | $this->assertEquals(['hello, world', 12], $arrayResult); 177 | } 178 | } 179 | 180 | class TestClassA 181 | { 182 | public function __construct(TestClassC $b) 183 | { 184 | } 185 | } 186 | 187 | class TestClassB 188 | { 189 | } 190 | 191 | class TestClassC 192 | { 193 | public function __construct(TestClassB $a) 194 | { 195 | } 196 | } 197 | 198 | class TestCircleDepClassA 199 | { 200 | public function __construct(TestCircleDepClassB $a) 201 | { 202 | } 203 | } 204 | 205 | class TestCircleDepClassB 206 | { 207 | public function __construct(TestCircleDepClassC $a) 208 | { 209 | } 210 | } 211 | 212 | class TestCircleDepClassC 213 | { 214 | public function __construct(TestCircleDepClassA $a) 215 | { 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /tests/Test/Router/RouteTest.php: -------------------------------------------------------------------------------- 1 | get('/a/b/c'); 17 | 18 | $this->assertTrue($route->matchUrl('/a/b/c')); 19 | $this->assertFalse($route->matchUrl('/a/b')); 20 | $this->assertFalse($route->matchUrl('/a/b/c/d')); 21 | $this->assertFalse($route->matchUrl('/a/b/c/')); 22 | $this->assertFalse($route->matchUrl('/b/c/')); 23 | $this->assertFalse($route->matchUrl('')); 24 | } 25 | 26 | public function testMatchRoute() 27 | { 28 | $route = new Route; 29 | $route->post('/a/{b}/c'); 30 | 31 | $this->assertTrue($route->matchUrl('/a/hello/c')); 32 | $this->assertArrayHasKey('b', $route->getParts()); 33 | $this->assertEquals('hello', $route->getParts()['b']); 34 | 35 | $this->assertFalse($route->matchUrl('/a/b')); 36 | $this->assertFalse($route->matchUrl('')); 37 | } 38 | 39 | public function testOptionalMatchRoute() 40 | { 41 | $route = new Route; 42 | $route->put('/a[/{b}[/{c}]]'); 43 | $route->matchUrl('/a/hello/world'); 44 | 45 | $this->assertArrayHasKey('b', $route->getParts()); 46 | $this->assertEquals('hello', $route->getParts()['b']); 47 | 48 | $this->assertArrayHasKey('c', $route->getParts()); 49 | $this->assertEquals('world', $route->getParts()['c']); 50 | 51 | $route = new Route; 52 | $route->put('/a[/{b}[/{c}]]'); 53 | $route->matchUrl('/a/hello'); 54 | 55 | $this->assertArrayHasKey('b', $route->getParts()); 56 | $this->assertEquals('hello', $route->getParts()['b']); 57 | 58 | $this->assertArrayNotHasKey('c', $route->getParts()); 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /tests/Test/Router/RouterTest.php: -------------------------------------------------------------------------------- 1 | get('/a/{b}/{c}') 18 | ->setTarget(function () { 19 | }) 20 | ->setName('abc'); 21 | $route2 = new Route; 22 | $route2->post('/') 23 | ->setTarget(function () { 24 | }) 25 | ->setName('slash'); 26 | $route3 = new Route; 27 | $route3->restful('/a/[{b}]') 28 | ->setTarget(function () { 29 | }) 30 | ->setName('ab'); 31 | $router->addRoute($route1); 32 | $router->addRoute($route2); 33 | $router->addRoute($route3); 34 | 35 | return [ 36 | [$router, $route1, $route2, $route3] 37 | ]; 38 | } 39 | 40 | /** 41 | * @dataProvider routerProvider 42 | * @param Router $router 43 | * @param Route $route1 44 | * @param Route $route2 45 | * @param Route $route3 46 | */ 47 | public function testRouter1(Router $router, Route $route1, Route $route2, Route $route3) 48 | { 49 | $route = $router->resolve('get', '/a/hello/world'); 50 | $this->assertSame($route1, $route); 51 | $this->assertEquals('hello', $route->getParts()->get('b')); 52 | } 53 | 54 | /** 55 | * @dataProvider routerProvider 56 | * @param Router $router 57 | * @param Route $route1 58 | * @param Route $route2 59 | * @param Route $route3 60 | */ 61 | public function testRouter2(Router $router, Route $route1, Route $route2, Route $route3) 62 | { 63 | $route = $router->resolve('post', '/'); 64 | $this->assertSame($route2, $route); 65 | } 66 | 67 | /** 68 | * @dataProvider routerProvider 69 | * @param Router $router 70 | * @param Route $route1 71 | * @param Route $route2 72 | * @param Route $route3 73 | */ 74 | public function testRouter3(Router $router, Route $route1, Route $route2, Route $route3) 75 | { 76 | $route = $router->resolve('GET', '/a/index'); 77 | $this->assertSame($route3, $route); 78 | } 79 | 80 | 81 | /** 82 | * @dataProvider routerProvider 83 | * @param Router $router 84 | * @param Route $route1 85 | * @param Route $route2 86 | * @param Route $route3 87 | */ 88 | public function testGeneratePath(Router $router, Route $route1, Route $route2, Route $route3) 89 | { 90 | $path = $router->generatePath('abc', ['b' => 'hello']); 91 | $this->assertEquals('/a/hello/{c}', $path); 92 | 93 | $path = $router->generatePath('abc', ['b' => 'hello', 'c' => 'world']); 94 | $this->assertEquals('/a/hello/world', $path); 95 | 96 | $path = $router->generatePath('ab', ['b' => 'hello']); 97 | $this->assertEquals('/a/hello', $path); 98 | 99 | $path = $router->generatePath('ab'); 100 | $this->assertEquals('/a/{b}', $path); 101 | } 102 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |