Skip to content

Commit 69ce84c

Browse files
committed
Use PHPStan 2.0
1 parent e2f5c4d commit 69ce84c

File tree

7 files changed

+92
-58
lines changed

7 files changed

+92
-58
lines changed

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"ext-fileinfo": "*",
3939
"ext-gd": "*",
4040
"icanboogie/datetime": "^3.0",
41-
"phpstan/phpstan": "^1.12",
41+
"phpstan/phpstan": "^2.0",
4242
"phpunit/phpunit": "^11.4"
4343
},
4444
"autoload": {

lib/File.php

+30-11
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
* @property-read string $type MIME type of the file.
3131
* @property-read int|false $size Size of the file.
3232
* @property-read int|null $error Error code, one of `UPLOAD_ERR_*`.
33-
* @property-read string $error_message A formatted message representing the error.
33+
* @property-read FormattedString|null $error_message A formatted message representing the error.
3434
* @property-read string $pathname Pathname of the file.
3535
* @property-read string $extension The extension of the file. If any, the dot is included e.g.
3636
* ".zip".
@@ -200,18 +200,16 @@ protected function get_error(): ?int
200200

201201
/**
202202
* Returns the message associated with the error.
203-
*
204-
* @return FormattedString|null
205203
*/
206-
protected function get_error_message()
204+
protected function get_error_message(): ?FormattedString
207205
{
208206
switch ($this->error) {
209207
case UPLOAD_ERR_OK:
210208
return null;
211209

212210
case UPLOAD_ERR_INI_SIZE:
213211
return $this->format("Maximum file size is :size Mb", [
214-
':size' => (int) ini_get('upload_max_filesize'),
212+
':size' => (int)ini_get('upload_max_filesize'),
215213
]);
216214

217215
case UPLOAD_ERR_FORM_SIZE:
@@ -259,10 +257,31 @@ protected function get_pathname(): ?string
259257
return $this->pathname ?? $this->tmp_name;
260258
}
261259

262-
protected function __construct(array $properties)
260+
private function __construct(array $properties)
263261
{
264262
foreach ($properties as $property => $value) {
265-
$this->$property = $value;
263+
switch ($property) {
264+
case self::OPTION_NAME:
265+
$this->name = $value;
266+
break;
267+
case self::OPTION_TYPE:
268+
$this->type = $value;
269+
break;
270+
case self::OPTION_SIZE:
271+
$this->size = $value;
272+
break;
273+
case self::OPTION_TMP_NAME:
274+
$this->tmp_name = $value;
275+
break;
276+
case self::OPTION_ERROR:
277+
$this->error = $value;
278+
break;
279+
case self::OPTION_PATHNAME:
280+
$this->pathname = $value;
281+
break;
282+
default:
283+
throw new \InvalidArgumentException("Unknown property: $property");
284+
}
266285
}
267286

268287
if (!$this->name && $this->pathname) {
@@ -297,7 +316,7 @@ public function to_array(): array
297316
$error_message = $this->error_message;
298317

299318
if ($error_message !== null) {
300-
$error_message = (string) $error_message;
319+
$error_message = (string)$error_message;
301320
}
302321

303322
return [
@@ -317,7 +336,7 @@ public function to_array(): array
317336
/**
318337
* Returns the extension of the file, if any.
319338
*
320-
* **Note:** The extension includes the dot e.g. ".zip". The extension is always in lower case.
339+
* **Note**: The extension includes the dot e.g. ".zip". The extension is always in lower case.
321340
*/
322341
protected function get_extension(): ?string
323342
{
@@ -367,7 +386,7 @@ public function match(array|string|null $type): bool
367386
}
368387

369388
if (!str_contains($type, '/')) {
370-
return (bool) preg_match('#^' . \preg_quote($type) . '/#', $this->type);
389+
return (bool)preg_match('#^' . \preg_quote($type) . '/#', $this->type);
371390
}
372391

373392
return $type === $this->type;
@@ -413,7 +432,7 @@ public function move(string $destination, bool $overwrite = self::MOVE_NO_OVERWR
413432
if ($this->pathname) {
414433
if (!rename($this->pathname, $destination)) {
415434
throw new \Exception(
416-
"Unable to move file to destination: $destination."
435+
"Unable to move file to destination: $destination.",
417436
); // @codeCoverageIgnore
418437
}
419438
}// @codeCoverageIgnoreStart

lib/FileInfo.php

+3-7
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class FileInfo
6464
* @return string The MIME type of the file, or `application/octet-stream` if it could not
6565
* be determined.
6666
*/
67-
public static function resolve_type(string $pathname, ?string &$extension = null): string
67+
public static function resolve_type(string $pathname, string &$extension = ''): string
6868
{
6969
$extension = '.' . \strtolower(\pathinfo($pathname, PATHINFO_EXTENSION));
7070
$types = self::TYPES;
@@ -79,14 +79,10 @@ public static function resolve_type(string $pathname, ?string &$extension = null
7979

8080
if ($type) {
8181
$alias = self::TYPES_ALIAS;
82-
return isset($alias[$type]) ? $alias[$type] : $type;
82+
return $alias[$type] ?? $type;
8383
}
8484
} // @codeCoverageIgnore
8585

86-
if (isset($types[$extension])) {
87-
return $types[$extension];
88-
}
89-
90-
return 'application/octet-stream'; // @codeCoverageIgnore
86+
return $types[$extension] ?? 'application/octet-stream';
9187
}
9288
}

lib/Headers/Header.php

-2
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,6 @@ public function __set(string $property, mixed $value): void
280280

281281
/**
282282
* Unsets the matching parameter.
283-
*
284-
* @throws PropertyNotDefined in attempt to access a parameter that is not defined.
285283
*/
286284
public function __unset(string $property): void
287285
{

lib/Request.php

+43-33
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@
5757
* @method Response trace(array $params = null)
5858
*
5959
* @property-read Request\Context $context the request's context.
60-
* @property-read FileList $files The files associated with the request.
61-
*
60+
* @property-read Headers $headers the request's headers.
61+
* @property-read FileList $files the request's files.
6262
* @property-read bool $authorization Authorization of the request.
6363
* @property-read int $content_length Length of the request content.
6464
* @property-read string $ip Remote IP of the request.
@@ -83,7 +83,7 @@ final class Request implements RequestOptions
8383
{
8484
/**
8585
* @uses get_context
86-
* @uses get_files
86+
* @uses get_headers
8787
* @uses get_script_name
8888
* @uses get_method
8989
* @uses get_query_string
@@ -126,22 +126,35 @@ final class Request implements RequestOptions
126126
/**
127127
* Union of {@see $path_params}, {@see $request_params} and {@see $query_params}.
128128
*
129-
* **Note:** The property is created during construct and is not updated after. If you modify one of
129+
* **Note**: The property is created during construct and is not updated after. If you modify one of
130130
* {@see $path_params}, {@see $request_params} and {@see $query_params}, remember to modify {@see $params} as
131131
* well.
132132
*
133133
* @var array<string, mixed>
134134
*/
135135
public array $params;
136136

137+
/**
138+
* TODO: The property should be readonly but cloning is only available from PHP 8.3:
139+
* https://www.php.net/releases/8.3/en.php#readonly_classes
140+
*/
137141
private Request\Context $context;
138142

139143
private function get_context(): Request\Context
140144
{
141145
return $this->context;
142146
}
143147

144-
public Headers $headers;
148+
/**
149+
* TODO: The property should be readonly but cloning is only available from PHP 8.3:
150+
* https://www.php.net/releases/8.3/en.php#readonly_classes
151+
*/
152+
private Headers $headers;
153+
154+
private function get_headers(): Headers
155+
{
156+
return $this->headers;
157+
}
145158

146159
/**
147160
* Request environment.
@@ -153,30 +166,26 @@ private function get_context(): Request\Context
153166
/**
154167
* Files associated with the request.
155168
*
156-
* @var FileList
169+
* **Note**: The field is not readonly because it can be overwritten by `with()`.
157170
*/
158-
private $files;
171+
private FileList $files;
159172

160173
private function get_files(): FileList
161174
{
162-
if ($this->files instanceof FileList) {
163-
return $this->files;
164-
}
165-
166-
return $this->files = FileList::from($this->files); // @phpstan-ignore-line
175+
return $this->files;
167176
}
168177

169178
public $cookie;
170179

171180
/**
172181
* A request may be created from the `$_SERVER` super global array. In that case `$_SERVER` is
173-
* used as environment the request is created with the following properties:
182+
* used as environment, the request is created with the following properties:
174183
*
175-
* - {@see $cookie}: a reference to the `$_COOKIE` super global array.
184+
* - {@see $cookie}: a reference to the `$_COOKIE` super global.
176185
* - {@see $path_params}: initialized to an empty array.
177-
* - {@see $query_params}: a reference to the `$_GET` super global array.
178-
* - {@see $request_params}: a reference to the `$_POST` super global array.
179-
* - {@see $files}: a reference to the `$_FILES` super global array.
186+
* - {@see $query_params}: a reference to the `$_GET` super global.
187+
* - {@see $request_params}: a reference to the `$_POST` super global.
188+
* - {@see $files}: a reference to the `$_FILES` super global.
180189
*
181190
* A request may also be created from an array of properties, in which case most of them are
182191
* mapped to the `$env` constructor param. For instance, `is_xhr` set the
@@ -191,9 +200,10 @@ private function get_files(): FileList
191200
* available in the environment are ignored.
192201
*
193202
* @phpstan-param array<RequestOptions::*, mixed>|string|null $properties Properties of the request.
203+
*
194204
* @param array<string, mixed> $env Environment, usually the `$_SERVER` array.
195205
*
196-
* @throws InvalidArgumentException in attempt to use an unsupported option.
206+
* @throws InvalidArgumentException in an attempt to use an unsupported option.
197207
*/
198208
public static function from(array|string|null $properties = null, array $env = []): self
199209
{
@@ -234,13 +244,13 @@ private static function from_server(): self
234244
self::OPTION_PATH_PARAMS => [],
235245
self::OPTION_QUERY_PARAMS => &$_GET,
236246
self::OPTION_REQUEST_PARAMS => $request_params,
237-
self::OPTION_FILES => &$_FILES // @codeCoverageIgnore
247+
self::OPTION_FILES => &$_FILES, // @codeCoverageIgnore
238248

239249
], $_SERVER);
240250
}
241251

242252
/**
243-
* Creates an instance from an URI.
253+
* Creates an instance from a URI.
244254
*
245255
* @param array<string, mixed> $env
246256
*/
@@ -260,7 +270,7 @@ private static function from_uri(string $uri, array $env): self
260270
private static function from_options(array $options, array $env): self
261271
{
262272
if ($options) {
263-
RequestOptionsMapper::map($options, $env);
273+
$options = RequestOptionsMapper::map($options, $env);
264274
}
265275

266276
if (!empty($env['QUERY_STRING'])) {
@@ -273,28 +283,28 @@ private static function from_options(array $options, array $env): self
273283
/**
274284
* Initialize the properties {@see $env}, {@see $headers} and {@see $context}.
275285
*
276-
* If the {@see $params} property is `null` it is set with an union of {@see $path_params},
286+
* If the {@see $params} property is `null` it is set with a union of {@see $path_params},
277287
* {@see $request_params} and {@see $query_params}.
278288
*
279-
* @phpstan-param array<string, mixed> $properties Initial properties.
289+
* @phpstan-param array<string, mixed> $options Initial properties.
280290
*
281291
* @param array<string, mixed> $env Environment of the request, usually the `$_SERVER` super global.
282292
*
283293
* @throws MethodNotAllowed when the request method is not supported.
284294
*/
285-
private function __construct(array $properties, array $env = [])
295+
private function __construct(array $options, array $env = [])
286296
{
287297
$this->context = new Request\Context($this);
288298
$this->env = $env;
289-
290-
foreach ($properties as $property => $value) {
291-
$this->$property = $value;
292-
}
299+
$this->headers = $options[self::OPTION_HEADERS] ?? new Headers($env);
300+
$this->files = $options[self::OPTION_FILES] ?? new FileList();
301+
$this->path_params = $options[self::OPTION_PATH_PARAMS] ?? [];
302+
$this->query_params = $options[self::OPTION_QUERY_PARAMS] ?? [];
303+
$this->request_params = $options[self::OPTION_REQUEST_PARAMS] ?? [];
304+
$this->params = $this->path_params + $this->request_params + $this->query_params;
305+
$this->cookie = $options[self::OPTION_COOKIE] ?? null;
293306

294307
$this->assert_method($this->method);
295-
296-
$this->headers ??= new Headers($env);
297-
$this->params = $this->path_params + $this->request_params + $this->query_params;
298308
}
299309

300310
/**
@@ -323,7 +333,7 @@ public function with(array $options): self
323333
$changed = clone $this;
324334

325335
if ($options) {
326-
RequestOptionsMapper::map($options, $changed->env);
336+
$options = RequestOptionsMapper::map($options, $changed->env);
327337

328338
foreach ($options as $option => &$value) {
329339
$changed->$option = $value;
@@ -434,7 +444,7 @@ private function get_is_local(): bool
434444
*
435445
* If defined, the `HTTP_X_FORWARDED_FOR` header is used to retrieve the original IP.
436446
*
437-
* If the `REMOTE_ADDR` header is empty the request is considered local thus `::1` is returned.
447+
* If the `REMOTE_ADDR` header is empty, the request is considered local; thus `::1` is returned.
438448
*
439449
* @link https://en.wikipedia.org/wiki/X-Forwarded-For
440450
*/

lib/RequestOptionsMapper.php

+13-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,17 @@ final class RequestOptionsMapper
2727
* @param array<string, mixed> $env Reference to the environment.
2828
*
2929
* @throws InvalidArgumentException on invalid option.
30+
*
31+
* @return array{
32+
* path_params?: array,
33+
* query_params?: array,
34+
* request_params?: array,
35+
* cookie?: string,
36+
* files?: FileList,
37+
* headers?: Headers
38+
* }
3039
*/
31-
public static function map(array &$options, array &$env): void
40+
public static function map(array $options, array &$env): array
3241
{
3342
foreach ($options as $option => &$value) {
3443
$mapper = self::get_value_mapper($option);
@@ -55,6 +64,8 @@ public static function map(array &$options, array &$env): void
5564

5665
throw new InvalidArgumentException("Option not supported: `$option`.");
5766
}
67+
68+
return $options;
5869
}
5970

6071
/**
@@ -68,7 +79,7 @@ private static function get_value_mapper(string $option): ?callable
6879
RequestOptions::OPTION_QUERY_PARAMS => fn($value) => $value,
6980
RequestOptions::OPTION_REQUEST_PARAMS => fn($value) => $value,
7081
RequestOptions::OPTION_COOKIE => fn($value) => $value,
71-
RequestOptions::OPTION_FILES => fn($value) => $value,
82+
RequestOptions::OPTION_FILES => fn($value) => ($value instanceof FileList) ? $value : new FileList($value),
7283
RequestOptions::OPTION_HEADERS => fn($value) => ($value instanceof Headers) ? $value : new Headers($value),
7384

7485
][$option] ?? null;

tests/RequestTest.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public static function provide_test_write_readonly_properties(): array
4444
$properties = 'authorization content_length context extension ip'
4545
. ' is_local is_xhr'
4646
. ' normalized_path method path port query_string referer script_name uri'
47-
. ' user_agent files';
47+
. ' user_agent';
4848

4949
return array_map(function ($name) {
5050
return (array) $name;
@@ -55,7 +55,7 @@ public function test_from_with_cache_control(): void
5555
{
5656
$value = "public, must-revalidate";
5757
$request = Request::from([ RequestOptions::OPTION_CACHE_CONTROL => $value ]);
58-
$this->assertFalse(isset($request->cache_control));
58+
$this->assertObjectNotHasProperty('cache_control', $request);
5959
$this->assertEquals('public', $request->headers->cache_control->cacheable);
6060
$this->assertTrue($request->headers->cache_control->must_revalidate);
6161
}

0 commit comments

Comments
 (0)