Skip to content

Commit 10a2be5

Browse files
authored
Extract & refactor mysqli TypeMapping (#262)
1 parent f591900 commit 10a2be5

File tree

3 files changed

+203
-206
lines changed

3 files changed

+203
-206
lines changed

src/QueryReflection/MysqliQueryReflector.php

Lines changed: 19 additions & 206 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,13 @@
88
use mysqli_result;
99
use mysqli_sql_exception;
1010
use PHPStan\ShouldNotHappenException;
11-
use PHPStan\Type\Accessory\AccessoryNumericStringType;
1211
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
1312
use PHPStan\Type\Constant\ConstantIntegerType;
1413
use PHPStan\Type\Constant\ConstantStringType;
15-
use PHPStan\Type\FloatType;
16-
use PHPStan\Type\IntegerType;
17-
use PHPStan\Type\IntersectionType;
18-
use PHPStan\Type\MixedType;
19-
use PHPStan\Type\StringType;
2014
use PHPStan\Type\Type;
21-
use PHPStan\Type\TypeCombinator;
22-
use PHPStan\Type\UnionType;
2315
use staabm\PHPStanDba\Error;
24-
use staabm\PHPStanDba\Types\MysqlIntegerRanges;
16+
use staabm\PHPStanDba\TypeMapping\MysqliTypeMapper;
17+
use staabm\PHPStanDba\TypeMapping\TypeMapper;
2518

2619
final class MysqliQueryReflector implements QueryReflector
2720
{
@@ -35,24 +28,12 @@ final class MysqliQueryReflector implements QueryReflector
3528

3629
private const MAX_CACHE_SIZE = 50;
3730

38-
/**
39-
* @var mysqli
40-
*/
41-
private $db;
31+
private mysqli $db;
4232

43-
/**
44-
* @var array<string, mysqli_sql_exception|list<object>|null>
45-
*/
46-
private $cache = [];
33+
/** @var array<string, mysqli_sql_exception|list<object>|null> */
34+
private array $cache = [];
4735

48-
/**
49-
* @var array<int, string>
50-
*/
51-
private $nativeTypes;
52-
/**
53-
* @var array<int, string>
54-
*/
55-
private $nativeFlags;
36+
private TypeMapper $typeMapper;
5637

5738
public function __construct(mysqli $mysqli)
5839
{
@@ -62,29 +43,7 @@ public function __construct(mysqli $mysqli)
6243
// enable exception throwing on php <8.1
6344
mysqli_report(\MYSQLI_REPORT_ERROR | \MYSQLI_REPORT_STRICT);
6445

65-
$this->nativeTypes = [];
66-
$this->nativeFlags = [];
67-
68-
$constants = get_defined_constants(true);
69-
foreach ($constants['mysqli'] as $c => $n) {
70-
if (!\is_int($n)) {
71-
// skip bool constants like MYSQLI_IS_MARIADB
72-
continue;
73-
}
74-
if (preg_match('/^MYSQLI_TYPE_(.*)/', $c, $m)) {
75-
if (!\is_string($m[1])) {
76-
throw new ShouldNotHappenException();
77-
}
78-
$this->nativeTypes[$n] = $m[1];
79-
} elseif (preg_match('/MYSQLI_(.*)_FLAG$/', $c, $m)) {
80-
if (!\is_string($m[1])) {
81-
throw new ShouldNotHappenException();
82-
}
83-
if (!\array_key_exists($n, $this->nativeFlags)) {
84-
$this->nativeFlags[$n] = $m[1];
85-
}
86-
}
87-
}
46+
$this->typeMapper = new MysqliTypeMapper();
8847
}
8948

9049
public function validateQueryString(string $queryString): ?Error
@@ -103,7 +62,10 @@ public function validateQueryString(string $queryString): ?Error
10362
$message = str_replace(' MariaDB server', ' MySQL/MariaDB server', $message);
10463

10564
// to ease debugging, print the error we simulated
106-
if (self::MYSQL_SYNTAX_ERROR_CODE === $e->getCode() && QueryReflection::getRuntimeConfiguration()->isDebugEnabled()) {
65+
if (
66+
self::MYSQL_SYNTAX_ERROR_CODE === $e->getCode()
67+
&& QueryReflection::getRuntimeConfiguration()->isDebugEnabled()
68+
) {
10769
$simulatedQuery = QuerySimulation::simulate($queryString);
10870
$message = $message."\n\nSimulated query: ".$simulatedQuery;
10971
}
@@ -128,20 +90,25 @@ public function getResultType(string $queryString, int $fetchType): ?Type
12890

12991
$i = 0;
13092
foreach ($result as $val) {
131-
if (!property_exists($val, 'name') || !property_exists($val, 'type') || !property_exists($val, 'flags') || !property_exists($val, 'length')) {
93+
if (
94+
!property_exists($val, 'name')
95+
|| !property_exists($val, 'type')
96+
|| !property_exists($val, 'flags')
97+
|| !property_exists($val, 'length')
98+
) {
13299
throw new ShouldNotHappenException();
133100
}
134101

135102
if (self::FETCH_TYPE_ASSOC === $fetchType || self::FETCH_TYPE_BOTH === $fetchType) {
136103
$arrayBuilder->setOffsetValueType(
137104
new ConstantStringType($val->name),
138-
$this->mapMysqlToPHPStanType($val->type, $val->flags, $val->length)
105+
$this->typeMapper->mapToPHPStanType($val->type, $val->flags, $val->length)
139106
);
140107
}
141108
if (self::FETCH_TYPE_NUMERIC === $fetchType || self::FETCH_TYPE_BOTH === $fetchType) {
142109
$arrayBuilder->setOffsetValueType(
143110
new ConstantIntegerType($i),
144-
$this->mapMysqlToPHPStanType($val->type, $val->flags, $val->length)
111+
$this->typeMapper->mapToPHPStanType($val->type, $val->flags, $val->length)
145112
);
146113
}
147114
++$i;
@@ -184,158 +151,4 @@ private function simulateQuery(string $queryString)
184151
return $this->cache[$queryString] = $e;
185152
}
186153
}
187-
188-
private function mapMysqlToPHPStanType(int $mysqlType, int $mysqlFlags, int $length): Type
189-
{
190-
$numeric = false;
191-
$notNull = false;
192-
$unsigned = false;
193-
$autoIncrement = false;
194-
195-
foreach ($this->flags2txt($mysqlFlags) as $flag) {
196-
switch ($flag) {
197-
case 'NUM':
198-
$numeric = true;
199-
break;
200-
201-
case 'NOT_NULL':
202-
$notNull = true;
203-
break;
204-
205-
case 'AUTO_INCREMENT':
206-
$autoIncrement = true;
207-
break;
208-
209-
case 'UNSIGNED':
210-
$unsigned = true;
211-
break;
212-
213-
// ???
214-
case 'PRI_KEY':
215-
case 'PART_KEY':
216-
case 'MULTIPLE_KEY':
217-
case 'NO_DEFAULT_VALUE':
218-
}
219-
}
220-
221-
$phpstanType = null;
222-
$mysqlIntegerRanges = new MysqlIntegerRanges();
223-
224-
if ($numeric) {
225-
if ($unsigned) {
226-
if (3 === $length) { // bool aka tinyint(1)
227-
$phpstanType = $mysqlIntegerRanges->unsignedTinyInt();
228-
}
229-
if (4 === $length) {
230-
$phpstanType = $mysqlIntegerRanges->unsignedTinyInt();
231-
}
232-
if (5 === $length) {
233-
$phpstanType = $mysqlIntegerRanges->unsignedSmallInt();
234-
}
235-
if (8 === $length) {
236-
$phpstanType = $mysqlIntegerRanges->unsignedMediumInt();
237-
}
238-
if (10 === $length) {
239-
$phpstanType = $mysqlIntegerRanges->unsignedInt();
240-
}
241-
if (20 === $length) {
242-
$phpstanType = $mysqlIntegerRanges->unsignedBigInt();
243-
}
244-
} else {
245-
if (1 == $length) {
246-
$phpstanType = $mysqlIntegerRanges->signedTinyInt();
247-
}
248-
if (4 === $length) {
249-
$phpstanType = $mysqlIntegerRanges->signedTinyInt();
250-
}
251-
if (6 === $length) {
252-
$phpstanType = $mysqlIntegerRanges->signedSmallInt();
253-
}
254-
if (9 === $length) {
255-
$phpstanType = $mysqlIntegerRanges->signedMediumInt();
256-
}
257-
if (11 === $length) {
258-
$phpstanType = $mysqlIntegerRanges->signedInt();
259-
}
260-
if (20 === $length) {
261-
$phpstanType = $mysqlIntegerRanges->signedBigInt();
262-
}
263-
if (22 === $length) {
264-
$phpstanType = $mysqlIntegerRanges->signedBigInt();
265-
}
266-
}
267-
}
268-
269-
if ($autoIncrement) {
270-
$phpstanType = $mysqlIntegerRanges->unsignedInt();
271-
}
272-
273-
if (null === $phpstanType) {
274-
switch ($this->type2txt($mysqlType)) {
275-
case 'DOUBLE':
276-
case 'NEWDECIMAL':
277-
$phpstanType = new FloatType();
278-
break;
279-
case 'LONGLONG':
280-
case 'LONG':
281-
case 'SHORT':
282-
case 'YEAR':
283-
case 'BIT':
284-
case 'INT24':
285-
$phpstanType = new IntegerType();
286-
break;
287-
case 'BLOB':
288-
case 'CHAR':
289-
case 'STRING':
290-
case 'VAR_STRING':
291-
case 'JSON':
292-
case 'DATE':
293-
case 'TIME':
294-
case 'DATETIME':
295-
case 'TIMESTAMP':
296-
$phpstanType = new StringType();
297-
break;
298-
default:
299-
$phpstanType = new MixedType();
300-
}
301-
}
302-
303-
if (QueryReflection::getRuntimeConfiguration()->isStringifyTypes()) {
304-
$numberType = new UnionType([new IntegerType(), new FloatType()]);
305-
$isNumber = $numberType->isSuperTypeOf($phpstanType)->yes();
306-
307-
if ($isNumber) {
308-
$phpstanType = new IntersectionType([
309-
new StringType(),
310-
new AccessoryNumericStringType(),
311-
]);
312-
}
313-
}
314-
315-
if (false === $notNull) {
316-
$phpstanType = TypeCombinator::addNull($phpstanType);
317-
}
318-
319-
return $phpstanType;
320-
}
321-
322-
private function type2txt(int $typeId): ?string
323-
{
324-
return \array_key_exists($typeId, $this->nativeTypes) ? $this->nativeTypes[$typeId] : null;
325-
}
326-
327-
/**
328-
* @return list<string>
329-
*/
330-
private function flags2txt(int $flagId): array
331-
{
332-
$result = [];
333-
foreach ($this->nativeFlags as $n => $t) {
334-
if ($flagId & $n) {
335-
$result[] = $t;
336-
}
337-
}
338-
339-
return $result;
340-
}
341154
}

0 commit comments

Comments
 (0)