Skip to content

Commit

Permalink
introduce PdoMysqlQueryReflector and deprecate PdoQueryReflector (#370)
Browse files Browse the repository at this point in the history
Co-authored-by: Markus Staab <[email protected]>
  • Loading branch information
staabm and clxmstaab authored May 23, 2022
1 parent d448d1f commit a5c7e09
Show file tree
Hide file tree
Showing 41 changed files with 183 additions and 161 deletions.
6 changes: 3 additions & 3 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# mark cache files as generated files, so they don't show up in Pull Requests diffs
.phpstan-dba-mysqli.cache linguist-generated=true
.phpstan-dba-pdo.cache linguist-generated=true
.phpstan-dba-pdo-mysql.cache linguist-generated=true
.phpunit-phpstan-dba-mysqli.cache linguist-generated=true
.phpunit-phpstan-dba-pdo.cache linguist-generated=true
.phpunit-phpstan-dba-pdo-mysql.cache linguist-generated=true
.phpunit-phpstan-dba-pdo-pgsql.cache linguist-generated=true

# Exclude non-essential files from dist
/tests export-ignore
.phpunit-phpstan-dba-mysqli.cache
.phpunit-phpstan-dba-pdo.cache
.phpunit-phpstan-dba-pdo-mysql.cache
.phpunit-phpstan-dba-pdo-pgsql.cache
4 changes: 2 additions & 2 deletions .github/workflows/phpstan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
include:
- php-version: "8.0"
db-image: 'mysql:8.0'
reflector: "pdo"
reflector: "pdo-mysql"
mode: "recording"
- php-version: "8.0"
db-image: 'mysql:8.0'
Expand Down Expand Up @@ -88,7 +88,7 @@ jobs:
reflector: "mysqli"
mode: "replay"
- php-version: "8.1"
reflector: "pdo"
reflector: "pdo-mysql"
mode: "replay"

env:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ jobs:
include:
- php-version: "7.4"
db-image: 'mysql:8.0'
reflector: "pdo"
reflector: "pdo-mysql"
mode: "recording"
- php-version: "8.0"
db-image: 'mysql:8.0'
reflector: "pdo"
reflector: "pdo-mysql"
mode: "recording"
- php-version: "8.0"
db-image: 'mysql:8.0'
Expand All @@ -40,7 +40,7 @@ jobs:

- php-version: "8.1"
db-image: 'mysql:8.0'
reflector: "pdo"
reflector: "pdo-mysql"
mode: "replay-and-recording"

env:
Expand Down Expand Up @@ -94,7 +94,7 @@ jobs:
matrix:
include:
- php-version: "8.1"
reflector: "pdo"
reflector: "pdo-mysql"
mode: "replay"
- php-version: "8.1"
reflector: "mysqli"
Expand Down
2 changes: 1 addition & 1 deletion .phpstan-dba-mysqli.cache

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .phpstan-dba-pdo.cache → .phpstan-dba-pdo-mysql.cache

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ QueryReflection::setupReflector(
ReflectionCache::create(
$cacheFile
),
// XXX alternatively you can use PdoQueryReflector instead
// XXX alternatively you can use PdoMysqlQueryReflector instead
new MysqliQueryReflector($mysqli),
new SchemaHasherMysql($mysqli)

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"@phpunit",
"@phpstan",

"@putenv DBA_REFLECTOR=pdo",
"@putenv DBA_REFLECTOR=pdo-mysql",
"@phpunit",
"@phpstan",

Expand Down
4 changes: 2 additions & 2 deletions docs/mysql.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Analyzing MySQL based codebases is supported for Doctrine DBAL, PDO and mysqli.

At analysis time you can pick either `MysqliQueryReflector` or `PdoQueryReflector`.
At analysis time you can pick either `MysqliQueryReflector` or `PdoMysqlQueryReflector`.

## Configuration

Expand Down Expand Up @@ -33,7 +33,7 @@ QueryReflection::setupReflector(
ReflectionCache::create(
$cacheFile
),
// XXX alternatively you can use PdoQueryReflector instead
// XXX alternatively you can use PdoMysqlQueryReflector instead
new MysqliQueryReflector($mysqli),
new SchemaHasherMysql($mysqli)

Expand Down
2 changes: 1 addition & 1 deletion docs/reflectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ It is **not** mandatory to use the same database driver for phpstan-dba, as you
| Reflector | Key Features |
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
| MysqliQueryReflector | - limited to mysql/mariadb databases<br/>- requires a active database connection<br/>- most feature complete reflector |
| PdoQueryReflector | - connects to a mysql/mariadb database<br/>- requires a active database connection |
| PdoMysqlQueryReflector | - connects to a mysql/mariadb database<br/>- requires a active database connection |
| PdoPgSqlQueryReflector | - connects to a PGSQL database<br/>- requires a active database connection |


Expand Down
2 changes: 1 addition & 1 deletion src/QueryReflection/BasePdoQueryReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
/**
* @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_* }
*/
abstract class BasePdoQueryReflector
abstract class BasePdoQueryReflector implements QueryReflector
{
private const PSQL_INVALID_TEXT_REPRESENTATION = '22P02';
private const PSQL_UNDEFINED_COLUMN = '42703';
Expand Down
125 changes: 125 additions & 0 deletions src/QueryReflection/PdoMysqlQueryReflector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

declare(strict_types=1);

namespace staabm\PHPStanDba\QueryReflection;

use Iterator;
use PDO;
use PDOException;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Type;
use staabm\PHPStanDba\TypeMapping\MysqlTypeMapper;
use staabm\PHPStanDba\TypeMapping\TypeMapper;

/**
* @phpstan-import-type ColumnMeta from BasePdoQueryReflector
*
* @final
*/
class PdoMysqlQueryReflector extends BasePdoQueryReflector
{
public function __construct(PDO $pdo)
{
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

parent::__construct($pdo, new MysqlTypeMapper());
}

/** @return PDOException|list<ColumnMeta>|null */
protected function simulateQuery(string $queryString)
{
if (\array_key_exists($queryString, $this->cache)) {
return $this->cache[$queryString];
}

if (\count($this->cache) > self::MAX_CACHE_SIZE) {
// make room for the next element by randomly removing a existing one
array_shift($this->cache);
}

$simulatedQuery = QuerySimulation::simulate($queryString);
if (null === $simulatedQuery) {
return $this->cache[$queryString] = null;
}

try {
$this->pdo->beginTransaction();
} catch (PDOException $e) {
// not all drivers may support transactions
}

try {
$stmt = $this->pdo->query($simulatedQuery);
} catch (PDOException $e) {
return $this->cache[$queryString] = $e;
} finally {
try {
$this->pdo->rollBack();
} catch (PDOException $e) {
// not all drivers may support transactions
}
}

$this->cache[$queryString] = [];
$columnCount = $stmt->columnCount();
$columnIndex = 0;
while ($columnIndex < $columnCount) {
$columnMeta = $stmt->getColumnMeta($columnIndex);

if (false === $columnMeta || !\array_key_exists('table', $columnMeta)) {
throw new ShouldNotHappenException('Failed to get column meta for column index '.$columnIndex);
}

// Native type may not be set, for example in case of JSON column.
if (!\array_key_exists('native_type', $columnMeta)) {
$columnMeta['native_type'] = \PDO::PARAM_INT === $columnMeta['pdo_type'] ? 'INT' : 'STRING';
}

$flags = $this->emulateFlags($columnMeta['native_type'], $columnMeta['table'], $columnMeta['name']);
foreach ($flags as $flag) {
$columnMeta['flags'][] = $flag;
}

// @phpstan-ignore-next-line
$this->cache[$queryString][$columnIndex] = $columnMeta;
++$columnIndex;
}

return $this->cache[$queryString];
}

/**
* @return Iterator<string, TypeMapper::FLAG_*>
*/
protected function checkInformationSchema(string $tableName): Iterator
{
if (null === $this->stmt) {
$this->stmt = $this->pdo->prepare(
// EXTRA, COLUMN_NAME seems to be nullable in mariadb
'SELECT
coalesce(COLUMN_NAME, "") as COLUMN_NAME,
coalesce(EXTRA, "") as EXTRA,
COLUMN_TYPE
FROM information_schema.columns
WHERE table_name = ? AND table_schema = DATABASE()'
);
}

$this->stmt->execute([$tableName]);
$result = $this->stmt->fetchAll(PDO::FETCH_ASSOC);

foreach ($result as $row) {
$extra = $row['EXTRA'];
$columnType = $row['COLUMN_TYPE'];
$columnName = $row['COLUMN_NAME'];

if (str_contains($extra, 'auto_increment')) {
yield $columnName => TypeMapper::FLAG_AUTO_INCREMENT;
}
if (str_contains($columnType, 'unsigned')) {
yield $columnName => TypeMapper::FLAG_UNSIGNED;
}
}
}
}
2 changes: 1 addition & 1 deletion src/QueryReflection/PdoPgSqlQueryReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
/**
* @phpstan-type PDOColumnMeta array{name: string, table?: string, native_type: string, len: int, flags: list<string>}
*/
final class PdoPgSqlQueryReflector extends BasePdoQueryReflector implements QueryReflector
final class PdoPgSqlQueryReflector extends BasePdoQueryReflector
{
public function __construct(PDO $pdo)
{
Expand Down
113 changes: 5 additions & 108 deletions src/QueryReflection/PdoQueryReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,120 +4,17 @@

namespace staabm\PHPStanDba\QueryReflection;

use Iterator;
use PDO;
use PDOException;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Type;
use staabm\PHPStanDba\TypeMapping\MysqlTypeMapper;
use staabm\PHPStanDba\TypeMapping\TypeMapper;

/**
* @phpstan-import-type ColumnMeta from BasePdoQueryReflector
* This class was kept for BC reasons.
*
* @deprected use PdoMysqlQueryReflector instead
*/
final class PdoQueryReflector extends BasePdoQueryReflector implements QueryReflector
final class PdoQueryReflector extends PdoMysqlQueryReflector // @phpstan-ignore-line
{
public function __construct(PDO $pdo)
{
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

parent::__construct($pdo, new MysqlTypeMapper());
}

/** @return PDOException|list<ColumnMeta>|null */
protected function simulateQuery(string $queryString)
{
if (\array_key_exists($queryString, $this->cache)) {
return $this->cache[$queryString];
}

if (\count($this->cache) > self::MAX_CACHE_SIZE) {
// make room for the next element by randomly removing a existing one
array_shift($this->cache);
}

$simulatedQuery = QuerySimulation::simulate($queryString);
if (null === $simulatedQuery) {
return $this->cache[$queryString] = null;
}

try {
$this->pdo->beginTransaction();
} catch (PDOException $e) {
// not all drivers may support transactions
}

try {
$stmt = $this->pdo->query($simulatedQuery);
} catch (PDOException $e) {
return $this->cache[$queryString] = $e;
} finally {
try {
$this->pdo->rollBack();
} catch (PDOException $e) {
// not all drivers may support transactions
}
}

$this->cache[$queryString] = [];
$columnCount = $stmt->columnCount();
$columnIndex = 0;
while ($columnIndex < $columnCount) {
$columnMeta = $stmt->getColumnMeta($columnIndex);

if (false === $columnMeta || !\array_key_exists('table', $columnMeta)) {
throw new ShouldNotHappenException('Failed to get column meta for column index '.$columnIndex);
}

// Native type may not be set, for example in case of JSON column.
if (!\array_key_exists('native_type', $columnMeta)) {
$columnMeta['native_type'] = \PDO::PARAM_INT === $columnMeta['pdo_type'] ? 'INT' : 'STRING';
}

$flags = $this->emulateFlags($columnMeta['native_type'], $columnMeta['table'], $columnMeta['name']);
foreach ($flags as $flag) {
$columnMeta['flags'][] = $flag;
}

// @phpstan-ignore-next-line
$this->cache[$queryString][$columnIndex] = $columnMeta;
++$columnIndex;
}

return $this->cache[$queryString];
}

/**
* @return Iterator<string, TypeMapper::FLAG_*>
*/
protected function checkInformationSchema(string $tableName): Iterator
{
if (null === $this->stmt) {
$this->stmt = $this->pdo->prepare(
// EXTRA, COLUMN_NAME seems to be nullable in mariadb
'SELECT
coalesce(COLUMN_NAME, "") as COLUMN_NAME,
coalesce(EXTRA, "") as EXTRA,
COLUMN_TYPE
FROM information_schema.columns
WHERE table_name = ? AND table_schema = DATABASE()'
);
}

$this->stmt->execute([$tableName]);
$result = $this->stmt->fetchAll(PDO::FETCH_ASSOC);

foreach ($result as $row) {
$extra = $row['EXTRA'];
$columnType = $row['COLUMN_TYPE'];
$columnName = $row['COLUMN_NAME'];

if (str_contains($extra, 'auto_increment')) {
yield $columnName => TypeMapper::FLAG_AUTO_INCREMENT;
}
if (str_contains($columnType, 'unsigned')) {
yield $columnName => TypeMapper::FLAG_UNSIGNED;
}
}
parent::__construct($pdo);
}
}
Loading

0 comments on commit a5c7e09

Please sign in to comment.