Completed
Push — master ( cc7341...ea60c3 )
by Anton
01:25
created

PSR7Client::fetchProtocolVersion()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.7666
c 0
b 0
f 0
cc 3
nc 3
nop 1
1
<?php
2
/**
3
 * High-performance PHP process supervisor and load balancer written in Go
4
 *
5
 * @author Wolfy-J
6
 */
7
8
namespace Spiral\RoadRunner;
9
10
use Http\Factory\Diactoros;
11
use Psr\Http\Message\ResponseInterface;
12
use Psr\Http\Message\ServerRequestFactoryInterface;
13
use Psr\Http\Message\ServerRequestInterface;
14
use Psr\Http\Message\StreamFactoryInterface;
15
use Psr\Http\Message\UploadedFileFactoryInterface;
16
17
/**
18
 * Manages PSR-7 request and response.
19
 */
20
class PSR7Client
21
{
22
    /** @var Worker */
23
    private $worker;
24
25
    /** @var ServerRequestFactoryInterface */
26
    private $requestFactory;
27
28
    /** @var StreamFactoryInterface */
29
    private $streamFactory;
30
31
    /*** @var UploadedFileFactoryInterface */
32
    private $uploadsFactory;
33
34
    /** @var array Valid values for HTTP protocol version */
35
    private static $allowedVersions = ['1.0', '1.1', '2',];
36
37
    /**
38
     * @param Worker                             $worker
39
     * @param ServerRequestFactoryInterface|null $requestFactory
40
     * @param StreamFactoryInterface|null        $streamFactory
41
     * @param UploadedFileFactoryInterface|null  $uploadsFactory
42
     */
43
    public function __construct(
44
        Worker $worker,
45
        ServerRequestFactoryInterface $requestFactory = null,
46
        StreamFactoryInterface $streamFactory = null,
47
        UploadedFileFactoryInterface $uploadsFactory = null
48
    ) {
49
        $this->worker = $worker;
50
        $this->requestFactory = $requestFactory ?? new Diactoros\ServerRequestFactory();
51
        $this->streamFactory = $streamFactory ?? new Diactoros\StreamFactory();
52
        $this->uploadsFactory = $uploadsFactory ?? new Diactoros\UploadedFileFactory();
53
    }
54
55
    /**
56
     * @return Worker
57
     */
58
    public function getWorker(): Worker
59
    {
60
        return $this->worker;
61
    }
62
63
    /**
64
     * @return ServerRequestInterface|null
65
     */
66
    public function acceptRequest()
67
    {
68
        $body = $this->worker->receive($ctx);
69
        if (empty($body) && empty($ctx)) {
70
            // termination request
71
            return null;
72
        }
73
74
        if (empty($ctx = json_decode($ctx, true))) {
75
            // invalid context
76
            return null;
77
        }
78
79
        $_SERVER = $this->configureServer($ctx);
80
81
        $request = $this->requestFactory->createServerRequest(
82
            $ctx['method'],
83
            $ctx['uri'],
84
            $_SERVER
85
        );
86
87
        parse_str($ctx['rawQuery'], $query);
88
89
        $request = $request
90
            ->withProtocolVersion(static::fetchProtocolVersion($ctx['protocol']))
0 ignored issues
show
Bug introduced by
Since fetchProtocolVersion() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of fetchProtocolVersion() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
91
            ->withCookieParams($ctx['cookies'])
92
            ->withQueryParams($query)
0 ignored issues
show
Bug introduced by
It seems like $query can also be of type null; however, Psr\Http\Message\ServerR...face::withQueryParams() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
93
            ->withUploadedFiles($this->wrapUploads($ctx['uploads']));
94
95
        foreach ($ctx['attributes'] as $name => $value) {
96
            $request = $request->withAttribute($name, $value);
97
        }
98
99
        foreach ($ctx['headers'] as $name => $value) {
100
            $request = $request->withHeader($name, $value);
101
        }
102
103
        if ($ctx['parsed']) {
104
            $request = $request->withParsedBody(json_decode($body, true));
105
        } else {
106
            if ($body !== null) {
107
                $request = $request->withBody($this->streamFactory->createStream($body));
0 ignored issues
show
Bug introduced by
It seems like $body defined by $this->worker->receive($ctx) on line 68 can also be of type object<Error>; however, Psr\Http\Message\StreamF...terface::createStream() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
108
            }
109
        }
110
111
        return $request;
112
    }
113
114
    /**
115
     * Send response to the application server.
116
     *
117
     * @param ResponseInterface $response
118
     */
119
    public function respond(ResponseInterface $response)
120
    {
121
        $headers = $response->getHeaders();
122
        if (empty($headers)) {
123
            // this is required to represent empty header set as map and not as array
124
            $headers = new \stdClass();
125
        }
126
127
        $this->worker->send($response->getBody(), json_encode([
128
            'status'  => $response->getStatusCode(),
129
            'headers' => $headers
130
        ]));
131
    }
132
133
    /**
134
     * Returns altered copy of _SERVER variable. Sets ip-address,
135
     * request-time and other values.
136
     *
137
     * @param array $ctx
138
     * @return array
139
     */
140
    protected function configureServer(array $ctx): array
141
    {
142
        $server = $_SERVER;
143
        $server['REQUEST_TIME'] = time();
144
        $server['REQUEST_TIME_FLOAT'] = microtime(true);
145
        $server['REMOTE_ADDR'] = $ctx['attributes']['ipAddress'] ?? $ctx['remoteAddr'] ?? '127.0.0.1';
146
        $server['REMOTE_ADDR'] = $ctx['attributes']['ipAddress'] ?? $ctx['remoteAddr'] ?? '127.0.0.1';
147
        $server['HTTP_USER_AGENT'] = $ctx['headers']['User-Agent'][0] ?? '';
148
149
        return $server;
150
    }
151
152
    /**
153
     * Wraps all uploaded files with UploadedFile.
154
     *
155
     * @param array $files
156
     *
157
     * @return array
158
     */
159
    private function wrapUploads($files): array
160
    {
161
        if (empty($files)) {
162
            return [];
163
        }
164
165
        $result = [];
166
        foreach ($files as $index => $f) {
167
            if (!isset($f['name'])) {
168
                $result[$index] = $this->wrapUploads($f);
169
                continue;
170
            }
171
172
            if (UPLOAD_ERR_OK === $f['error']) {
173
                $stream = $this->streamFactory->createStreamFromFile($f['tmpName']);
174
            } else {
175
                $stream = $this->streamFactory->createStream();
176
            }
177
178
            $result[$index] = $this->uploadsFactory->createUploadedFile(
179
                $stream,
180
                $f['size'],
181
                $f['error'],
182
                $f['name'],
183
                $f['mime']
184
            );
185
        }
186
187
        return $result;
188
    }
189
190
    /**
191
     * Normalize HTTP protocol version to valid values
192
     *
193
     * @param string $version
194
     * @return string
195
     */
196
    private static function fetchProtocolVersion(string $version): string
197
    {
198
        $v = substr($version, 5);
199
200
        if ($v === '2.0') {
201
            return '2';
202
        }
203
204
        // Fallback for values outside of valid protocol versions
205
        if (!in_array($v, static::$allowedVersions, true)) {
0 ignored issues
show
Bug introduced by
Since $allowedVersions is declared private, accessing it with static will lead to errors in possible sub-classes; consider using self, or increasing the visibility of $allowedVersions to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return static::$someVariable;
    }
}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass { }

YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class SomeClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return self::$someVariable; // self works fine with private.
    }
}
Loading history...
206
            return '1.1';
207
        }
208
209
        return $v;
210
    }
211
}
212