PSR7Client::acceptRequest()   B
last analyzed

Complexity

Conditions 6
Paths 13

Size

Total Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 41
rs 8.6417
c 0
b 0
f 0
cc 6
nc 13
nop 0
1
<?php
2
3
/**
4
 * High-performance PHP process supervisor and load balancer written in Go
5
 *
6
 * @author Wolfy-J
7
 */
8
declare(strict_types=1);
9
10
namespace Spiral\RoadRunner;
11
12
use Psr\Http\Message\ResponseInterface;
13
use Psr\Http\Message\ServerRequestFactoryInterface;
14
use Psr\Http\Message\ServerRequestInterface;
15
use Psr\Http\Message\StreamFactoryInterface;
16
use Psr\Http\Message\UploadedFileFactoryInterface;
17
use Psr\Http\Message\UploadedFileInterface;
18
19
/**
20
 * Manages PSR-7 request and response.
21
 */
22
class PSR7Client
23
{
24
    /** @var HttpClient */
25
    private $httpClient;
26
27
    /** @var ServerRequestFactoryInterface */
28
    private $requestFactory;
29
30
    /** @var StreamFactoryInterface */
31
    private $streamFactory;
32
33
    /** @var UploadedFileFactoryInterface */
34
    private $uploadsFactory;
35
36
    /** @var mixed[] */
37
    private $originalServer = [];
38
39
    /** @var string[] Valid values for HTTP protocol version */
40
    private static $allowedVersions = ['1.0', '1.1', '2',];
41
42
    /**
43
     * @param Worker                             $worker
44
     * @param ServerRequestFactoryInterface|null $requestFactory
45
     * @param StreamFactoryInterface|null        $streamFactory
46
     * @param UploadedFileFactoryInterface|null  $uploadsFactory
47
     */
48
    public function __construct(
49
        Worker $worker,
50
        ServerRequestFactoryInterface $requestFactory = null,
51
        StreamFactoryInterface $streamFactory = null,
52
        UploadedFileFactoryInterface $uploadsFactory = null
53
    ) {
54
        $this->httpClient = new HttpClient($worker);
55
        $this->requestFactory = $requestFactory ?? new Diactoros\ServerRequestFactory();
56
        $this->streamFactory = $streamFactory ?? new Diactoros\StreamFactory();
57
        $this->uploadsFactory = $uploadsFactory ?? new Diactoros\UploadedFileFactory();
58
        $this->originalServer = $_SERVER;
59
    }
60
61
    /**
62
     * @return Worker
63
     */
64
    public function getWorker(): Worker
65
    {
66
        return $this->httpClient->getWorker();
67
    }
68
69
    /**
70
     * @return ServerRequestInterface|null
71
     */
72
    public function acceptRequest(): ?ServerRequestInterface
73
    {
74
        $rawRequest = $this->httpClient->acceptRequest();
75
        if ($rawRequest === null) {
76
            return null;
77
        }
78
79
        $_SERVER = $this->configureServer($rawRequest['ctx']);
80
81
        $request = $this->requestFactory->createServerRequest(
82
            $rawRequest['ctx']['method'],
83
            $rawRequest['ctx']['uri'],
84
            $_SERVER
85
        );
86
87
        parse_str($rawRequest['ctx']['rawQuery'], $query);
88
89
        $request = $request
90
            ->withProtocolVersion(static::fetchProtocolVersion($rawRequest['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($rawRequest['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($rawRequest['ctx']['uploads']));
94
95
        foreach ($rawRequest['ctx']['attributes'] as $name => $value) {
96
            $request = $request->withAttribute($name, $value);
97
        }
98
99
        foreach ($rawRequest['ctx']['headers'] as $name => $value) {
100
            $request = $request->withHeader($name, $value);
101
        }
102
103
        if ($rawRequest['ctx']['parsed']) {
104
            return $request->withParsedBody(json_decode($rawRequest['body'], true));
105
        }
106
107
        if ($rawRequest['body'] !== null) {
108
            return $request->withBody($this->streamFactory->createStream($rawRequest['body']));
0 ignored issues
show
Bug introduced by
It seems like $rawRequest['body'] 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...
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): void
120
    {
121
        $this->httpClient->respond(
122
            $response->getStatusCode(),
123
            $response->getBody()->__toString(),
124
            $response->getHeaders()
125
        );
126
    }
127
128
    /**
129
     * Returns altered copy of _SERVER variable. Sets ip-address,
130
     * request-time and other values.
131
     *
132
     * @param mixed[] $ctx
133
     * @return mixed[]
134
     */
135
    protected function configureServer(array $ctx): array
136
    {
137
        $server = $this->originalServer;
138
139
        $server['REQUEST_URI'] = $ctx['uri'];
140
        $server['REQUEST_TIME'] = time();
141
        $server['REQUEST_TIME_FLOAT'] = microtime(true);
142
        $server['REMOTE_ADDR'] = $ctx['attributes']['ipAddress'] ?? $ctx['remoteAddr'] ?? '127.0.0.1';
143
        $server['REQUEST_METHOD'] = $ctx['method'];
144
145
        $server['HTTP_USER_AGENT'] = '';
146
        foreach ($ctx['headers'] as $key => $value) {
147
            $key = strtoupper(str_replace('-', '_', $key));
148
            if (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH'])) {
149
                $server[$key] = implode(', ', $value);
150
            } else {
151
                $server['HTTP_' . $key] = implode(', ', $value);
152
            }
153
        }
154
155
        return $server;
156
    }
157
158
    /**
159
     * Wraps all uploaded files with UploadedFile.
160
     *
161
     * @param array[] $files
162
     *
163
     * @return UploadedFileInterface[]|mixed[]
164
     */
165
    private function wrapUploads($files): array
166
    {
167
        if (empty($files)) {
168
            return [];
169
        }
170
171
        $result = [];
172
        foreach ($files as $index => $f) {
173
            if (!isset($f['name'])) {
174
                $result[$index] = $this->wrapUploads($f);
0 ignored issues
show
Documentation introduced by
$f is of type array<string,null,{"name":"null"}>, but the function expects a array<integer,array>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
175
                continue;
176
            }
177
178
            if (UPLOAD_ERR_OK === $f['error']) {
179
                $stream = $this->streamFactory->createStreamFromFile($f['tmpName']);
180
            } else {
181
                $stream = $this->streamFactory->createStream();
182
            }
183
184
            $result[$index] = $this->uploadsFactory->createUploadedFile(
185
                $stream,
186
                $f['size'],
187
                $f['error'],
188
                $f['name'],
189
                $f['mime']
190
            );
191
        }
192
193
        return $result;
194
    }
195
196
    /**
197
     * Normalize HTTP protocol version to valid values
198
     *
199
     * @param string $version
200
     * @return string
201
     */
202
    private static function fetchProtocolVersion(string $version): string
203
    {
204
        $v = substr($version, 5);
205
206
        if ($v === '2.0') {
207
            return '2';
208
        }
209
210
        // Fallback for values outside of valid protocol versions
211
        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...
212
            return '1.1';
213
        }
214
215
        return $v;
216
    }
217
}
218