Skip to content

Commit 505242b

Browse files
staabmPavel Karfikclxmstaab
authored
extracted BasePdoQueryReflector (#338)
* PgsqlQueryReflector base * run actions * rename * Refactor PdoPgsqlQueryReflector with pgsql type resolving * cs * move const * comment * drop file from rebase * cs * chage type * us smallint for adaid * Delete PdoPgSqlQueryReflector.php * Delete PgsqlTypeMapper.php * Delete PgsqlIntegerRanges.php * Update ReflectorFactory.php * adapt PdoQueryReflector to BasePdoQueryReflector * fix typo * fix * fix * fix * fix * Update PdoQueryReflector.php * cs Co-authored-by: Pavel Karfik <pavel.karfik@cdn77.com> Co-authored-by: Markus Staab <m.staab@complex-it.de>
1 parent adc9dec commit 505242b

File tree

7 files changed

+213
-167
lines changed

7 files changed

+213
-167
lines changed

.phpstan-dba-mysqli.cache

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Error.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
namespace staabm\PHPStanDba;
44

5+
use staabm\PHPStanDba\QueryReflection\BasePdoQueryReflector;
56
use staabm\PHPStanDba\QueryReflection\MysqliQueryReflector;
6-
use staabm\PHPStanDba\QueryReflection\PdoQueryReflector;
77
use staabm\PHPStanDba\QueryReflection\QuerySimulation;
88

99
/**
10-
* @phpstan-type ErrorCodes value-of<MysqliQueryReflector::MYSQL_ERROR_CODES>|value-of<PDOQueryReflector::PDO_ERROR_CODES>
10+
* @phpstan-type ErrorCodes value-of<MysqliQueryReflector::MYSQL_ERROR_CODES>|value-of<BasePdoQueryReflector::PDO_ERROR_CODES>
1111
*/
1212
final class Error
1313
{
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace staabm\PHPStanDba\QueryReflection;
6+
7+
use Iterator;
8+
use PDO;
9+
use PDOException;
10+
use PDOStatement;
11+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
12+
use PHPStan\Type\Constant\ConstantIntegerType;
13+
use PHPStan\Type\Constant\ConstantStringType;
14+
use PHPStan\Type\Type;
15+
use staabm\PHPStanDba\Error;
16+
use staabm\PHPStanDba\TypeMapping\TypeMapper;
17+
18+
/**
19+
* @phpstan-type ColumnMeta array{name: string, table: string, native_type: string, len: int, flags: list<string>}
20+
*/
21+
abstract class BasePdoQueryReflector
22+
{
23+
private const PSQL_INVALID_TEXT_REPRESENTATION = '22P02';
24+
private const PSQL_UNDEFINED_COLUMN = '42703';
25+
private const PSQL_UNDEFINED_TABLE = '42P01';
26+
27+
private const MYSQL_SYNTAX_ERROR_CODE = '42000';
28+
private const MYSQL_UNKNOWN_COLUMN_IN_FIELDLIST = '42S22';
29+
private const MYSQL_UNKNOWN_TABLE = '42S02';
30+
31+
private const PDO_SYNTAX_ERROR_CODES = [
32+
self::MYSQL_SYNTAX_ERROR_CODE,
33+
self::PSQL_INVALID_TEXT_REPRESENTATION,
34+
];
35+
36+
private const PDO_ERROR_CODES = [
37+
self::PSQL_INVALID_TEXT_REPRESENTATION,
38+
self::PSQL_UNDEFINED_COLUMN,
39+
self::PSQL_UNDEFINED_TABLE,
40+
self::MYSQL_SYNTAX_ERROR_CODE,
41+
self::MYSQL_UNKNOWN_COLUMN_IN_FIELDLIST,
42+
self::MYSQL_UNKNOWN_TABLE,
43+
];
44+
45+
protected const MAX_CACHE_SIZE = 50;
46+
47+
/**
48+
* @var array<string, PDOException|list<ColumnMeta>|null>
49+
*/
50+
protected array $cache = [];
51+
52+
protected TypeMapper $typeMapper;
53+
54+
// @phpstan-ignore-next-line
55+
protected ?PDOStatement $stmt = null;
56+
/**
57+
* @var array<string, array<string, list<string>>>
58+
*/
59+
protected array $emulatedFlags = [];
60+
61+
public function __construct(protected PDO $pdo, TypeMapper $typeMapper)
62+
{
63+
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
64+
65+
$this->typeMapper = $typeMapper;
66+
}
67+
68+
public function validateQueryString(string $queryString): ?Error
69+
{
70+
$result = $this->simulateQuery($queryString);
71+
72+
if (!$result instanceof PDOException) {
73+
return null;
74+
}
75+
76+
$e = $result;
77+
if (\in_array($e->getCode(), self::PDO_ERROR_CODES, true)) {
78+
if (
79+
\in_array($e->getCode(), self::PDO_SYNTAX_ERROR_CODES, true)
80+
&& QueryReflection::getRuntimeConfiguration()->isDebugEnabled()
81+
) {
82+
return Error::forSyntaxError($e, $e->getCode(), $queryString);
83+
}
84+
85+
return Error::forException($e, $e->getCode());
86+
}
87+
88+
return null;
89+
}
90+
91+
/**
92+
* @param QueryReflector::FETCH_TYPE* $fetchType
93+
*/
94+
public function getResultType(string $queryString, int $fetchType): ?Type
95+
{
96+
$result = $this->simulateQuery($queryString);
97+
98+
if (!\is_array($result)) {
99+
return null;
100+
}
101+
102+
$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
103+
104+
$i = 0;
105+
foreach ($result as $val) {
106+
if (QueryReflector::FETCH_TYPE_ASSOC === $fetchType || QueryReflector::FETCH_TYPE_BOTH === $fetchType) {
107+
$arrayBuilder->setOffsetValueType(
108+
new ConstantStringType($val['name']),
109+
$this->typeMapper->mapToPHPStanType($val['native_type'], $val['flags'], $val['len'])
110+
);
111+
}
112+
if (QueryReflector::FETCH_TYPE_NUMERIC === $fetchType || QueryReflector::FETCH_TYPE_BOTH === $fetchType) {
113+
$arrayBuilder->setOffsetValueType(
114+
new ConstantIntegerType($i),
115+
$this->typeMapper->mapToPHPStanType($val['native_type'], $val['flags'], $val['len'])
116+
);
117+
}
118+
++$i;
119+
}
120+
121+
return $arrayBuilder->getArray();
122+
}
123+
124+
/**
125+
* @return list<string>
126+
*/
127+
protected function emulateFlags(string $nativeType, string $tableName, string $columnName): array
128+
{
129+
if (\array_key_exists($tableName, $this->emulatedFlags)) {
130+
$emulatedFlags = [];
131+
if (\array_key_exists($columnName, $this->emulatedFlags[$tableName])) {
132+
$emulatedFlags = $this->emulatedFlags[$tableName][$columnName];
133+
}
134+
135+
if ($this->typeMapper->isNumericCol($nativeType)) {
136+
$emulatedFlags[] = TypeMapper::FLAG_NUMERIC;
137+
}
138+
139+
return $emulatedFlags;
140+
}
141+
142+
$this->emulatedFlags[$tableName] = [];
143+
144+
// determine flags of all columns of the given table once
145+
$schemaFlags = $this->checkInformationSchema($tableName);
146+
foreach ($schemaFlags as $schemaColumnName => $flag) {
147+
if (!\array_key_exists($schemaColumnName, $this->emulatedFlags[$tableName])) {
148+
$this->emulatedFlags[$tableName][$schemaColumnName] = [];
149+
}
150+
$this->emulatedFlags[$tableName][$schemaColumnName][] = $flag;
151+
}
152+
153+
return $this->emulateFlags($nativeType, $tableName, $columnName);
154+
}
155+
156+
/** @return PDOException|list<ColumnMeta>|null */
157+
abstract protected function simulateQuery(string $queryString);
158+
159+
/** @return Iterator<string, TypeMapper::FLAG_*> */
160+
abstract protected function checkInformationSchema(string $tableName);
161+
}

src/QueryReflection/PdoQueryReflector.php

Lines changed: 12 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -7,126 +7,25 @@
77
use Iterator;
88
use PDO;
99
use PDOException;
10-
use PDOStatement;
1110
use PHPStan\ShouldNotHappenException;
12-
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
13-
use PHPStan\Type\Constant\ConstantIntegerType;
14-
use PHPStan\Type\Constant\ConstantStringType;
1511
use PHPStan\Type\Type;
16-
use staabm\PHPStanDba\Error;
1712
use staabm\PHPStanDba\TypeMapping\MysqlTypeMapper;
18-
use function strtoupper;
13+
use staabm\PHPStanDba\TypeMapping\TypeMapper;
1914

2015
/**
21-
* @phpstan-type ColumnMeta array{name: string, table: string, native_type: string, len: int, flags: array<int, string>, precision: int<0, max>, pdo_type: PDO::PARAM_* }
16+
* @phpstan-import-type ColumnMeta from BasePdoQueryReflector
2217
*/
23-
final class PdoQueryReflector implements QueryReflector
18+
final class PdoQueryReflector extends BasePdoQueryReflector implements QueryReflector
2419
{
25-
private const PSQL_INVALID_TEXT_REPRESENTATION = '22P02';
26-
private const PSQL_UNDEFINED_COLUMN = '42703';
27-
private const PSQL_UNDEFINED_TABLE = '42P01';
28-
29-
private const MYSQL_SYNTAX_ERROR_CODE = '42000';
30-
private const MYSQL_UNKNOWN_COLUMN_IN_FIELDLIST = '42S22';
31-
private const MYSQL_UNKNOWN_TABLE = '42S02';
32-
33-
private const PDO_SYNTAX_ERROR_CODES = [
34-
self::MYSQL_SYNTAX_ERROR_CODE,
35-
self::PSQL_INVALID_TEXT_REPRESENTATION,
36-
];
37-
38-
private const PDO_ERROR_CODES = [
39-
self::PSQL_INVALID_TEXT_REPRESENTATION,
40-
self::PSQL_UNDEFINED_COLUMN,
41-
self::PSQL_UNDEFINED_TABLE,
42-
self::MYSQL_SYNTAX_ERROR_CODE,
43-
self::MYSQL_UNKNOWN_COLUMN_IN_FIELDLIST,
44-
self::MYSQL_UNKNOWN_TABLE,
45-
];
46-
47-
private const MAX_CACHE_SIZE = 50;
48-
49-
/**
50-
* @var array<string, PDOException|array<int<0, max>, ColumnMeta>|null>
51-
*/
52-
private array $cache = [];
53-
54-
private MysqlTypeMapper $typeMapper;
55-
56-
/**
57-
* @var PDOStatement<array{COLUMN_TYPE: string, COLUMN_NAME: string, EXTRA: string}>|null
58-
*/
59-
private ?PDOStatement $stmt = null;
60-
/**
61-
* @var array<string, array<string, list<string>>>
62-
*/
63-
private array $emulatedFlags = [];
64-
65-
public function __construct(private PDO $pdo)
20+
public function __construct(PDO $pdo)
6621
{
67-
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
22+
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
6823

69-
$this->typeMapper = new MysqlTypeMapper();
70-
}
71-
72-
public function validateQueryString(string $queryString): ?Error
73-
{
74-
$result = $this->simulateQuery($queryString);
75-
76-
if (!$result instanceof PDOException) {
77-
return null;
78-
}
79-
80-
$e = $result;
81-
if (\in_array($e->getCode(), self::PDO_ERROR_CODES, true)) {
82-
if (
83-
\in_array($e->getCode(), self::PDO_SYNTAX_ERROR_CODES, true)
84-
&& QueryReflection::getRuntimeConfiguration()->isDebugEnabled()
85-
) {
86-
return Error::forSyntaxError($e, $e->getCode(), $queryString);
87-
}
88-
89-
return Error::forException($e, $e->getCode());
90-
}
91-
92-
return null;
93-
}
94-
95-
/**
96-
* @param self::FETCH_TYPE* $fetchType
97-
*/
98-
public function getResultType(string $queryString, int $fetchType): ?Type
99-
{
100-
$result = $this->simulateQuery($queryString);
101-
102-
if (!\is_array($result)) {
103-
return null;
104-
}
105-
106-
$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
107-
108-
$i = 0;
109-
foreach ($result as $val) {
110-
if (self::FETCH_TYPE_ASSOC === $fetchType || self::FETCH_TYPE_BOTH === $fetchType) {
111-
$arrayBuilder->setOffsetValueType(
112-
new ConstantStringType($val['name']),
113-
$this->typeMapper->mapToPHPStanType($val['native_type'], $val['flags'], $val['len'])
114-
);
115-
}
116-
if (self::FETCH_TYPE_NUMERIC === $fetchType || self::FETCH_TYPE_BOTH === $fetchType) {
117-
$arrayBuilder->setOffsetValueType(
118-
new ConstantIntegerType($i),
119-
$this->typeMapper->mapToPHPStanType($val['native_type'], $val['flags'], $val['len'])
120-
);
121-
}
122-
++$i;
123-
}
124-
125-
return $arrayBuilder->getArray();
24+
parent::__construct($pdo, new MysqlTypeMapper());
12625
}
12726

12827
/** @return PDOException|list<ColumnMeta>|null */
129-
private function simulateQuery(string $queryString)
28+
protected function simulateQuery(string $queryString)
13029
{
13130
if (\array_key_exists($queryString, $this->cache)) {
13231
return $this->cache[$queryString];
@@ -172,7 +71,7 @@ private function simulateQuery(string $queryString)
17271
throw new ShouldNotHappenException('Failed to get column meta for column index '.$columnIndex);
17372
}
17473

175-
$flags = $this->emulateMysqlFlags($columnMeta['native_type'], $columnMeta['table'], $columnMeta['name']);
74+
$flags = $this->emulateFlags($columnMeta['native_type'], $columnMeta['table'], $columnMeta['name']);
17675
foreach ($flags as $flag) {
17776
$columnMeta['flags'][] = $flag;
17877
}
@@ -186,41 +85,9 @@ private function simulateQuery(string $queryString)
18685
}
18786

18887
/**
189-
* @return list<string>
88+
* @return Iterator<string, TypeMapper::FLAG_*>
19089
*/
191-
private function emulateMysqlFlags(string $mysqlType, string $tableName, string $columnName): array
192-
{
193-
if (\array_key_exists($tableName, $this->emulatedFlags)) {
194-
$emulatedFlags = [];
195-
if (\array_key_exists($columnName, $this->emulatedFlags[$tableName])) {
196-
$emulatedFlags = $this->emulatedFlags[$tableName][$columnName];
197-
}
198-
199-
if ($this->isNumericCol($mysqlType)) {
200-
$emulatedFlags[] = MysqlTypeMapper::FLAG_NUMERIC;
201-
}
202-
203-
return $emulatedFlags;
204-
}
205-
206-
$this->emulatedFlags[$tableName] = [];
207-
208-
// determine flags of all columns of the given table once
209-
$schemaFlags = $this->checkInformationSchema($tableName);
210-
foreach ($schemaFlags as $schemaColumnName => $flag) {
211-
if (!\array_key_exists($schemaColumnName, $this->emulatedFlags[$tableName])) {
212-
$this->emulatedFlags[$tableName][$schemaColumnName] = [];
213-
}
214-
$this->emulatedFlags[$tableName][$schemaColumnName][] = $flag;
215-
}
216-
217-
return $this->emulateMysqlFlags($mysqlType, $tableName, $columnName);
218-
}
219-
220-
/**
221-
* @return Iterator<string, MysqlTypeMapper::FLAG_*>
222-
*/
223-
private function checkInformationSchema(string $tableName): Iterator
90+
protected function checkInformationSchema(string $tableName): Iterator
22491
{
22592
if (null === $this->stmt) {
22693
$this->stmt = $this->pdo->prepare(
@@ -243,25 +110,11 @@ private function checkInformationSchema(string $tableName): Iterator
243110
$columnName = $row['COLUMN_NAME'];
244111

245112
if (str_contains($extra, 'auto_increment')) {
246-
yield $columnName => MysqlTypeMapper::FLAG_AUTO_INCREMENT;
113+
yield $columnName => TypeMapper::FLAG_AUTO_INCREMENT;
247114
}
248115
if (str_contains($columnType, 'unsigned')) {
249-
yield $columnName => MysqlTypeMapper::FLAG_UNSIGNED;
116+
yield $columnName => TypeMapper::FLAG_UNSIGNED;
250117
}
251118
}
252119
}
253-
254-
private function isNumericCol(string $mysqlType): bool
255-
{
256-
return match (strtoupper($mysqlType)) {
257-
'LONGLONG',
258-
'LONG',
259-
'SHORT',
260-
'TINY',
261-
'YEAR',
262-
'BIT',
263-
'INT24' => true,
264-
default => false,
265-
};
266-
}
267120
}

0 commit comments

Comments
 (0)