6

从零实现一个 PHP 微框架 - 初始化请求

 2 years ago
source link: https://blog.ixk.me/post/implement-a-php-microframework-from-zero-6
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
本文最后更新于 269 天前,文中所描述的信息可能已发生改变

更新一波文章。

这次的内容相对简单点,初始化请求的过程包括封装 $_GET $_POST 等关联数组到 Request 对象中,用于后续流程的使用,以及从封装 Request 到路由之前的这段过程。

构造 Request

构造 Request 是通过 Application.dispatchToEmit 里的 $request = $this->make(Request::class) 初始化的,make 方法会通知容器初始化 Request 对象。

1<?php
2protected function dispatchToEmit(): void
3{
4    // 获取请求
5    $request = $this->make(Request::class);
6
7    // 处理
8    $response = $this->make(RouteManager::class)->dispatch($request);
9
10    // 发送响应
11    $response->send();
12}

既然是通过容器来初始化的,那么就需要绑定该对象到容器,Request 对象是通过 RequestProvider 进行绑定的:

1<?php
2
3namespace App\Providers;
4
5use App\Http\Request;
6
7class RequestProvider extends Provider
8{
9    public function register(): void
10    {
11        $this->app->singleton(
12            Request::class,
13            function () {
14                return Request::make();
15            },
16            'request'
17        );
18    }
19}

Request::make

从上面的代码可以看到初始化 Request 是通过 Request::make() 的静态工厂方法构造的:

1<?php
2public static function make(
3    array $server = null,
4    array $query = null,
5    array $body = null,
6    array $cookies = null,
7    array $files = null
8): Request {
9    $files = Functions::convertFiles($files ?: $_FILES);
10    $server = $server ?: $_SERVER;
11    $uri =
12        isset($server['HTTPS']) && $server['HTTPS'] === 'on'
13            ? 'https://'
14            : 'http://';
15    if (isset($server['HTTP_HOST'])) {
16        $uri .= $server['HTTP_HOST'];
17    } else {
18        $uri .=
19            $server['SERVER_NAME'] .
20            (isset($server['SERVER_PORT']) &&
21            $server['SERVER_PORT'] !== '80' &&
22            $server['SERVER_PORT'] !== '443'
23                ? ':' . $server['SERVER_PORT']
24                : '');
25    }
26    $uri .= $server['REQUEST_URI'];
27    $protocol = '1.1';
28    if (isset($server['SERVER_PROTOCOL'])) {
29        preg_match(
30            '|^(HTTP/)?(?P<version>[1-9]\d*(?:\.\d)?)$|',
31            $server['SERVER_PROTOCOL'],
32            $matches
33        );
34        $protocol = $matches['version'];
35    }
36    return new static(
37        $server,
38        $files,
39        $uri,
40        $server['REQUEST_METHOD'],
41        'php://input',
42        Functions::parseHeaders($server),
43        $cookies ?: $_COOKIE,
44        $query ?: $_GET,
45        $body ?: $_POST,
46        $protocol
47    );
48}

首先是使用 Functions::convertFiles 方法将 $_FILES 关联数组转化到 UploadFile 数组,转化的步骤就不说明了,就是将数组的结构封装到对象(之所以要这么做是为了遵循 PSR 标准)。

1<?php
2public static function convertFiles(array $files): array
3{
4    $result = [];
5    foreach ($files as $key => $value) {
6        if ($value instanceof UploadedFileInterface) {
7            $result[$key] = $value;
8            continue;
9        }
10        if (
11            is_array($value) &&
12            isset($value['tmp_name']) &&
13            is_array($value['tmp_name'])
14        ) {
15            $result[$key] = self::resolveStructure($value);
16            continue;
17        }
18        if (is_array($value) && isset($value['tmp_name'])) {
19            $result[$key] = new UploadFile(
20                $value['tmp_name'],
21                $value['size'],
22                $value['error'],
23                $value['name'],
24                $value['type']
25            );
26            continue;
27        }
28        if (is_array($value)) {
29            $result[$key] = self::convertFiles($value);
30            continue;
31        }
32    }
33    return $result;
34}

然后是拼接 URL,由于 PHP 已经对 URL 进行切割,所以我们还需要拼接回去,以便后续的代码使用。以及 Protocol 的提取。

由于 PHP 将 Request header 存入了 $_SERVER 为了方便使用,我们需要把 $_SERVER 中带有 HTTP_ 前缀的字段都提取出来,这些就是 Request header,同时由于 header 是不区分大小写的,我们直接把 header 的名称转成小写即可。

1<?php
2public static function parseHeaders(array $server): array
3{
4    $headers = [];
5    foreach ($server as $key => $value) {
6        if (!is_string($key)) {
7            continue;
8        }
9        if ($value === '') {
10            continue;
11        }
12        if (strpos($key, 'HTTP_') === 0) {
13            $name = str_replace('_', '-', strtolower(substr($key, 5)));
14            $headers[$name] = $value;
15            continue;
16        }
17    }
18    return $headers;
19}

new Request

有了上面的一些基础的信息,就可以正式的创建 Request 对象的:

1<?php
2public function __construct(
3    array $server = [],
4    array $files = [],
5    $uri = '',
6    string $method = 'GET',
7    $body = 'php://input',
8    array $headers = [],
9    array $cookies = [],
10    array $query = [],
11    $parsed_body = null,
12    string $protocol = '1.1'
13) {
14    $this->validateFiles($files);
15    if ($body === 'php://input') {
16        $body = new Stream($body);
17    }
18    $this->setMethod($method);
19    if ($uri instanceof UriInterface) {
20        $this->uri = $uri;
21    } else {
22        $this->uri = new Uri($uri);
23    }
24    if ($body instanceof StreamInterface) {
25        $this->stream = $body;
26    } else {
27        $this->stream = new Stream($body, 'wb+');
28    }
29    $this->setHeaders($headers);
30    $this->server = $server;
31    $this->files = $files;
32    $this->cookies = $cookies;
33    $this->query = $query;
34    $this->protocol = $protocol;
35
36    $content_type = $this->header('Content-Type');
37    if (
38        $content_type !== null &&
39        stripos($content_type, 'application/json') !== false
40    ) {
41        $this->parsed_body = array_merge(
42            $parsed_body,
43            json_decode($this->stream->getContents(), true)
44        );
45    } else {
46        $this->parsed_body = $parsed_body;
47    }
48
49    if (!$this->hasHeader('Host') && $this->uri->getHost()) {
50        $host = $this->uri->getHost();
51        $host .= $this->uri->getPort() ? ':' . $this->uri->getPort() : '';
52        $this->headerAlias['host'] = 'Host';
53        $this->headers['Host'] = [$host];
54    }
55}

首先需要对 files 进行验证,判断 files 是否实现了 UploadedFileInterface

接着就需要对 Request body 进行封装了,StreamStreamInterface 的实现类,提供了对 body 数据流的一些操作方法。

除了 filebody,我们还需要把 url 封装成 Uri 对象,该对象实现了 UriInterface,提供了对 url 的一些操作方法。

由于请求的方式可能是通过 JSON 的格式传输的,此时 $_POST 就无法获取到这些通过 JSON 传输的数据,所以,我们还需要解析 JSON。

在 PSR 标准中有说明,当请求没有 Host 头的时候,需要手动设置,保证 Request 对象中存在 Host 头。

到这里初始化 Request 的部分就完成了。由于博主忙着重写 XK-Editor,所以更新 文章的速度可能会慢一点 2333。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK