Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/phpstan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
mysql -uroot -h127.0.0.1 -proot -e 'create database phpstan_dba;'
mysql -uroot -h127.0.0.1 -proot phpstan_dba < tests/schema.sql

- run: vendor/bin/phpstan
- run: composer phpstan

replay:
name: PHPStan (reflection replay)
Expand Down Expand Up @@ -84,4 +84,4 @@ jobs:
with:
composer-options: "--prefer-dist --no-progress"

- run: vendor/bin/phpstan
- run: composer phpstan -- --debug
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
mysql -uroot -h127.0.0.1 -proot -e 'create database phpstan_dba;'
mysql -uroot -h127.0.0.1 -proot phpstan_dba < tests/schema.sql

- run: vendor/bin/phpunit
- run: composer phpunit

replay:
name: PHPUnit (reflection replay)
Expand Down Expand Up @@ -88,4 +88,4 @@ jobs:
- name: Setup Problem Matchers for PHPUnit
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"

- run: vendor/bin/phpunit
- run: composer phpunit -- --debug
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ In case you are using Doctrine ORM, you might use phpstan-dba in tandem with [ph
**Note:**
At the moment only mysql/mariadb databases are supported. Technically it's not a big problem to support other databases though.

[see the unit-testsuite](https://github.com/staabm/phpstan-dba/tree/main/tests/data) to get a feeling about the current featureset.
[see the unit-testsuite](https://github.com/staabm/phpstan-dba/tree/main/tests/default/data) to get a feeling about the current featureset.


## DEMO
Expand Down Expand Up @@ -49,6 +49,7 @@ $cacheFile = __DIR__.'/.phpstan-dba.cache';

$config = new RuntimeConfiguration();
// $config->debugMode(true);
// $config->stringifyTypes(true);

QueryReflection::setupReflector(
new RecordingQueryReflector(
Expand Down Expand Up @@ -92,6 +93,7 @@ Within your `phpstan-dba-bootstrap.php` file you can configure `phpstan-dba` so
Use the [`RuntimeConfiguration`](https://github.com/staabm/phpstan-dba/tree/main/src/QueryReflection/RuntimeConfiguration.php) builder-object and pass it as a second argument to `QueryReflection::setupReflector()`.

If not configured otherwise, the following defaults are used:
- type-inference works as precise as possible. In case your database access layer returns strings instead of integers and floats, use the [`stringifyTypes`](https://github.com/staabm/phpstan-dba/tree/main/src/QueryReflection/RuntimeConfiguration.php) option.
- when analyzing a php8+ codebase, [`PDO::ERRMODE_EXCEPTION` error handling](https://www.php.net/manual/en/pdo.error-handling.php) is assumed.
- when analyzing a php8.1+ codebase, [`mysqli_report(\MYSQLI_REPORT_ERROR | \MYSQLI_REPORT_STRICT);` error handling](https://www.php.net/mysqli_report) is assumed.

Expand All @@ -116,6 +118,7 @@ $cacheFile = __DIR__.'/.phpstan-dba.cache';

$config = new RuntimeConfiguration();
// $config->debugMode(true);
// $config->stringifyTypes(true);

QueryReflection::setupReflector(
new ReplayQueryReflector(
Expand Down
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,13 @@
"@csfix"
],
"phpstan": [
"phpstan analyse -c phpstan.neon.dist"
"phpstan analyse -c phpstan.neon.dist",
"phpstan analyse -c tests/default/config/phpstan.neon.dist",
"phpstan analyse -c tests/stringify/config/phpstan.neon.dist"
],
"phpunit": [
"phpunit"
"phpunit -c tests/default/config/phpunit.xml",
"phpunit -c tests/stringify/config/phpunit.xml"
]
},
"config": {
Expand Down
22 changes: 0 additions & 22 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,6 @@ parameters:

paths:
- src/
- tests/

bootstrapFiles:
- bootstrap.php

ignoreErrors:
-
message: '#Function Deployer\\runMysqlQuery\(\) should return array<int, array<int, string>>\|null but return statement is missing.#'
path: tests/default/data/runMysqlQuery.php
-
message: '#.*has no return type specified.#'
path: tests/*
-
message: '#.*with no type specified.#'
path: tests/*
-
message: '#.*return type has no value type specified in iterable type iterable.#'
path: tests/*
-
message: '#.*with no value type specified in iterable type array.#'
path: tests/*

excludePaths:
analyseAndScan:
- *Fixture/**
15 changes: 15 additions & 0 deletions src/QueryReflection/MysqliQueryReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@
use mysqli_result;
use mysqli_sql_exception;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Accessory\AccessoryNumericStringType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
use staabm\PHPStanDba\Error;
use staabm\PHPStanDba\Types\MysqlIntegerRanges;

Expand Down Expand Up @@ -295,6 +298,18 @@ private function mapMysqlToPHPStanType(int $mysqlType, int $mysqlFlags, int $len
}
}

if (QueryReflection::getRuntimeConfiguration()->isStringifyTypes()) {
$numberType = new UnionType([new IntegerType(), new FloatType()]);
$isNumber = $numberType->isSuperTypeOf($phpstanType)->yes();

if ($isNumber) {
$phpstanType = new IntersectionType([
new StringType(),
new AccessoryNumericStringType(),
]);
}
}

if (false === $notNull) {
$phpstanType = TypeCombinator::addNull($phpstanType);
}
Expand Down
81 changes: 65 additions & 16 deletions src/QueryReflection/ReflectionCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
namespace staabm\PHPStanDba\QueryReflection;

use const LOCK_EX;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Type;
use staabm\PHPStanDba\DbaException;
use staabm\PHPStanDba\Error;

final class ReflectionCache
{
public const SCHEMA_VERSION = 'v3-rename-props';
public const SCHEMA_VERSION = 'v4-runtime-config';

/**
* @var string
Expand All @@ -28,6 +29,11 @@ final class ReflectionCache
*/
private $changes = [];

/**
* @var bool
*/
private $initialized = false;

/**
* @var resource
*/
Expand Down Expand Up @@ -55,19 +61,40 @@ public static function create(string $cacheFile): self
return new self($cacheFile);
}

/**
* @deprecated use create() instead
*/
public static function load(string $cacheFile): self
{
$reflectionCache = new self($cacheFile);
$cachedRecords = $reflectionCache->readCache(true);
if (null !== $cachedRecords) {
$reflectionCache->records = $cachedRecords;
return new self($cacheFile);
}

/**
* @return array<string, array{error?: ?Error, result?: array<QueryReflector::FETCH_TYPE*, ?Type>}>
*/
private function lazyReadRecords()
{
if ($this->initialized) {
return $this->records;
}

$cache = $this->readCache(true);
if (null !== $cache) {
$this->records = $cache['records'];
} else {
$this->records = [];
}
$this->initialized = true;

return $reflectionCache;
return $this->records;
}

/**
* @return array<string, array{error?: ?Error, result?: array<QueryReflector::FETCH_TYPE*, ?Type>}>|null
* @return array{
* records: array<string, array{error?: ?Error, result?: array<QueryReflector::FETCH_TYPE*, ?Type>}>,
* runtimeConfig: array<string, scalar>,
* schemaVersion: string
* }|null
*/
private function readCache(bool $useReadLock): ?array
{
Expand All @@ -88,11 +115,20 @@ private function readCache(bool $useReadLock): ?array
}
}

if (\is_array($cache) && \array_key_exists('schemaVersion', $cache) && self::SCHEMA_VERSION === $cache['schemaVersion']) {
return $cache['records'];
if (!\is_array($cache) || !\array_key_exists('schemaVersion', $cache) || self::SCHEMA_VERSION !== $cache['schemaVersion']) {
return null;
}

if ($cache['runtimeConfig'] !== QueryReflection::getRuntimeConfiguration()->toArray()) {
return null;
}

return null;
if (!\is_array($cache['records'])) {
throw new ShouldNotHappenException();
}

// @phpstan-ignore-next-line
return $cache;
}

public function persist(): void
Expand Down Expand Up @@ -122,6 +158,7 @@ public function persist(): void
$cacheContent = '<?php return '.var_export([
'schemaVersion' => self::SCHEMA_VERSION,
'records' => $newRecords,
'runtimeConfig' => QueryReflection::getRuntimeConfiguration()->toArray(),
], true).';';

if (false === file_put_contents($this->cacheFile, $cacheContent)) {
Expand All @@ -139,7 +176,9 @@ public function persist(): void

public function hasValidationError(string $queryString): bool
{
if (!\array_key_exists($queryString, $this->records)) {
$records = $this->lazyReadRecords();

if (!\array_key_exists($queryString, $records)) {
return false;
}

Expand All @@ -150,7 +189,9 @@ public function hasValidationError(string $queryString): bool

public function getValidationError(string $queryString): ?Error
{
if (!\array_key_exists($queryString, $this->records)) {
$records = $this->lazyReadRecords();

if (!\array_key_exists($queryString, $records)) {
throw new DbaException(sprintf('Cache not populated for query "%s"', $queryString));
}

Expand All @@ -164,7 +205,9 @@ public function getValidationError(string $queryString): ?Error

public function putValidationError(string $queryString, ?Error $error): void
{
if (!\array_key_exists($queryString, $this->records)) {
$records = $this->lazyReadRecords();

if (!\array_key_exists($queryString, $records)) {
$this->changes[$queryString] = $this->records[$queryString] = [];
}

Expand All @@ -178,7 +221,9 @@ public function putValidationError(string $queryString, ?Error $error): void
*/
public function hasResultType(string $queryString, int $fetchType): bool
{
if (!\array_key_exists($queryString, $this->records)) {
$records = $this->lazyReadRecords();

if (!\array_key_exists($queryString, $records)) {
return false;
}

Expand All @@ -195,7 +240,9 @@ public function hasResultType(string $queryString, int $fetchType): bool
*/
public function getResultType(string $queryString, int $fetchType): ?Type
{
if (!\array_key_exists($queryString, $this->records)) {
$records = $this->lazyReadRecords();

if (!\array_key_exists($queryString, $records)) {
throw new DbaException(sprintf('Cache not populated for query "%s"', $queryString));
}

Expand All @@ -216,7 +263,9 @@ public function getResultType(string $queryString, int $fetchType): ?Type
*/
public function putResultType(string $queryString, int $fetchType, ?Type $resultType): void
{
if (!\array_key_exists($queryString, $this->records)) {
$records = $this->lazyReadRecords();

if (!\array_key_exists($queryString, $records)) {
$this->changes[$queryString] = $this->records[$queryString] = [];
}

Expand Down
34 changes: 34 additions & 0 deletions src/QueryReflection/RuntimeConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,19 @@ final class RuntimeConfiguration
* @var bool
*/
private $debugMode = false;
/**
* @var bool
*/
private $stringifyTypes = false;

public static function create(): self
{
return new self();
}

/**
* Defines whether the database access returns `false` on error or throws exceptions.
*
* @param self::ERROR_MODE* $mode
*/
public function errorMode(string $mode): self
Expand All @@ -54,11 +60,27 @@ public function debugMode(bool $mode): self
return $this;
}

/**
* Infer string-types instead of more precise types.
* This might be necessary in case your are using `\PDO::ATTR_EMULATE_PREPARES` or `\PDO::ATTR_STRINGIFY_FETCHES`.
*/
public function stringifyTypes(bool $stringify): self
{
$this->stringifyTypes = $stringify;

return $this;
}

public function isDebugEnabled(): bool
{
return $this->debugMode;
}

public function isStringifyTypes(): bool
{
return $this->stringifyTypes;
}

public function throwsPdoExceptions(PhpVersion $phpVersion): bool
{
if (self::ERROR_MODE_EXCEPTION === $this->errorMode) {
Expand All @@ -84,4 +106,16 @@ public function throwsMysqliExceptions(PhpVersion $phpVersion): bool
// since php8.1 the mysqli php-src default error mode changed to exception
return $phpVersion->getVersionId() >= 80100;
}

/**
* @return array<string, scalar>
*/
public function toArray(): array
{
return [
'errorMode' => $this->errorMode,
'debugMode' => $this->debugMode,
'stringifyTypes' => $this->stringifyTypes,
];
}
}
Loading