Skip to content

Commit 909ec00

Browse files
staabmclxmstaab
andauthored
refactor MysqliQueryReflector to reduce numer of executed queries (#103)
Co-authored-by: Markus Staab <[email protected]>
1 parent 303d826 commit 909ec00

File tree

2 files changed

+74
-36
lines changed

2 files changed

+74
-36
lines changed

.phpunit-phpstan-dba.cache

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@
9191
array (
9292
'error' => NULL,
9393
),
94+
'
95+
SELECT email, adaid
96+
FROM ada
97+
WHERE gesperrt = \'1\'
98+
LIMIT \'1\', \'1\'
99+
' =>
100+
array (
101+
'error' => NULL,
102+
),
94103
'
95104
SELECT email, adaid
96105
FROM ada

src/QueryReflection/MysqliQueryReflector.php

Lines changed: 65 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use mysqli;
88
use mysqli_result;
99
use mysqli_sql_exception;
10+
use PHPStan\ShouldNotHappenException;
1011
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
1112
use PHPStan\Type\Constant\ConstantIntegerType;
1213
use PHPStan\Type\Constant\ConstantStringType;
@@ -26,11 +27,18 @@ final class MysqliQueryReflector implements QueryReflector
2627

2728
public const MYSQL_HOST_NOT_FOUND = 2002;
2829

30+
private const MAX_CACHE_SIZE = 50;
31+
2932
/**
3033
* @var mysqli
3134
*/
3235
private $db;
3336

37+
/**
38+
* @var array<string, mysqli_sql_exception|list<object>|null>
39+
*/
40+
private $cache = [];
41+
3442
/**
3543
* @var array<int, string>
3644
*/
@@ -65,62 +73,49 @@ public function __construct(mysqli $mysqli)
6573

6674
public function validateQueryString(string $queryString): ?Error
6775
{
68-
$simulatedQuery = QuerySimulation::simulate($queryString);
69-
if (null === $simulatedQuery) {
76+
$result = $this->simulateQuery($queryString);
77+
if (!$result instanceof mysqli_sql_exception) {
7078
return null;
7179
}
80+
$e = $result;
7281

73-
try {
74-
$this->db->query($simulatedQuery);
82+
if (\in_array($e->getCode(), [self::MYSQL_SYNTAX_ERROR_CODE, self::MYSQL_UNKNOWN_COLUMN_IN_FIELDLIST, self::MYSQL_UNKNOWN_TABLE], true)) {
83+
$message = $e->getMessage();
7584

76-
return null;
77-
} catch (mysqli_sql_exception $e) {
78-
if (\in_array($e->getCode(), [self::MYSQL_SYNTAX_ERROR_CODE, self::MYSQL_UNKNOWN_COLUMN_IN_FIELDLIST, self::MYSQL_UNKNOWN_TABLE], true)) {
79-
$message = $e->getMessage();
80-
81-
// make error string consistent across mysql/mariadb
82-
$message = str_replace(' MySQL server', ' MySQL/MariaDB server', $message);
83-
$message = str_replace(' MariaDB server', ' MySQL/MariaDB server', $message);
85+
// make error string consistent across mysql/mariadb
86+
$message = str_replace(' MySQL server', ' MySQL/MariaDB server', $message);
87+
$message = str_replace(' MariaDB server', ' MySQL/MariaDB server', $message);
8488

85-
// to ease debugging, print the error we simulated
86-
if (self::MYSQL_SYNTAX_ERROR_CODE === $e->getCode() && QueryReflection::getRuntimeConfiguration()->isDebugEnabled()) {
87-
$message = $message."\n\nSimulated query: ".$simulatedQuery;
88-
}
89-
90-
return new Error($message, $e->getCode());
89+
// to ease debugging, print the error we simulated
90+
if (self::MYSQL_SYNTAX_ERROR_CODE === $e->getCode() && QueryReflection::getRuntimeConfiguration()->isDebugEnabled()) {
91+
$simulatedQuery = QuerySimulation::simulate($queryString);
92+
$message = $message."\n\nSimulated query: ".$simulatedQuery;
9193
}
9294

93-
return null;
95+
return new Error($message, $e->getCode());
9496
}
97+
98+
return null;
9599
}
96100

97101
/**
98102
* @param self::FETCH_TYPE* $fetchType
99103
*/
100104
public function getResultType(string $queryString, int $fetchType): ?Type
101105
{
102-
$simulatedQuery = QuerySimulation::simulate($queryString);
103-
if (null === $simulatedQuery) {
104-
return null;
105-
}
106-
107-
try {
108-
$result = $this->db->query($simulatedQuery);
109-
110-
if (!$result instanceof mysqli_result) {
111-
return null;
112-
}
113-
} catch (mysqli_sql_exception $e) {
106+
$result = $this->simulateQuery($queryString);
107+
if (!\is_array($result)) {
114108
return null;
115109
}
116110

117111
$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
118112

119-
/* Get field information for all result-columns */
120-
$finfo = $result->fetch_fields();
121-
122113
$i = 0;
123-
foreach ($finfo as $val) {
114+
foreach ($result as $val) {
115+
if (!property_exists($val, 'name') || !property_exists($val, 'type') || !property_exists($val, 'flags') || !property_exists($val, 'length')) {
116+
throw new ShouldNotHappenException();
117+
}
118+
124119
if (self::FETCH_TYPE_ASSOC === $fetchType || self::FETCH_TYPE_BOTH === $fetchType) {
125120
$arrayBuilder->setOffsetValueType(
126121
new ConstantStringType($val->name),
@@ -135,11 +130,45 @@ public function getResultType(string $queryString, int $fetchType): ?Type
135130
}
136131
++$i;
137132
}
138-
$result->free();
139133

140134
return $arrayBuilder->getArray();
141135
}
142136

137+
/**
138+
* @return mysqli_sql_exception|list<object>|null
139+
*/
140+
private function simulateQuery(string $queryString)
141+
{
142+
if (\array_key_exists($queryString, $this->cache)) {
143+
return $this->cache[$queryString];
144+
}
145+
146+
if (\count($this->cache) > self::MAX_CACHE_SIZE) {
147+
// make room for the next element by randomly removing a existing one
148+
array_shift($this->cache);
149+
}
150+
151+
$simulatedQuery = QuerySimulation::simulate($queryString);
152+
if (null === $simulatedQuery) {
153+
return $this->cache[$queryString] = null;
154+
}
155+
156+
try {
157+
$result = $this->db->query($simulatedQuery);
158+
159+
if (!$result instanceof mysqli_result) {
160+
return $this->cache[$queryString] = null;
161+
}
162+
163+
$resultInfo = $result->fetch_fields();
164+
$result->free();
165+
166+
return $this->cache[$queryString] = $resultInfo;
167+
} catch (mysqli_sql_exception $e) {
168+
return $this->cache[$queryString] = $e;
169+
}
170+
}
171+
143172
private function mapMysqlToPHPStanType(int $mysqlType, int $mysqlFlags, int $length): Type
144173
{
145174
$numeric = false;

0 commit comments

Comments
 (0)