PHPackages                             pierresh/phpstan-pdo-mysql - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. pierresh/phpstan-pdo-mysql

ActivePhpstan-extension[Database &amp; ORM](/categories/database)

pierresh/phpstan-pdo-mysql
==========================

PHPStan rules for validating PDO/MySQL code: SQL syntax, parameter bindings, and SELECT columns matching PHPDoc types

2.0.0(1mo ago)1176MITPHPPHP ^8.1

Since Nov 1Pushed 1mo agoCompare

[ Source](https://github.com/pierresh/phpstan-pdo-mysql)[ Packagist](https://packagist.org/packages/pierresh/phpstan-pdo-mysql)[ RSS](/packages/pierresh-phpstan-pdo-mysql/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (10)Dependencies (18)Versions (27)Used By (0)

PHPStan PDO MySQL Rules
=======================

[](#phpstan-pdo-mysql-rules)

Static analysis rules for PHPStan that validate PDO/MySQL code for common errors that would otherwise only be caught at runtime.

Features
--------

[](#features)

This extension provides seven powerful rules that work without requiring a database connection:

1. **SQL Syntax Validation** - Detects MySQL syntax errors in `prepare()` and `query()` calls
2. **Parameter Binding Validation** - Ensures PDO parameters match SQL placeholders
3. **SELECT Column Validation** - Verifies SELECT columns match PHPDoc type annotations
4. **Self-Reference Detection** - Catches self-reference conditions in JOIN and WHERE clauses
5. **Invalid Table Reference Detection** - Catches typos in table/alias names (e.g., `user.name` when table is `users`)
6. **Tautological Condition Detection** - Catches always-true/false conditions like `WHERE 1 = 1`
7. **MySQL-Specific Syntax Detection** - Flags MySQL-specific functions that have portable ANSI alternatives

All validation is performed statically by analyzing your code, so no database setup is needed.

**Developer Tools:**

- **`ddt()` Helper Function** - Generates PHPStan type definitions from runtime values for easy copy-paste into your code
- **`ddc()` Helper Function** - Generates PHP class definitions from objects for use with `PDO::fetchObject()`

Installation
------------

[](#installation)

```
composer require --dev pierresh/phpstan-pdo-mysql
```

The extension will be automatically registered if you use [phpstan/extension-installer](https://github.com/phpstan/extension-installer).

Manual registration in `phpstan.neon`:

```
includes:
    - vendor/pierresh/phpstan-pdo-mysql/extension.neon
```

Examples
--------

[](#examples)

### 1. SQL Syntax Validation

[](#1-sql-syntax-validation)

Catches syntax errors in SQL queries:

```
// ❌ Incomplete query
$stmt = $db->query("SELECT * FROM");
```

Caution

SQL syntax error in query(): Expected token NAME ~RESERVED, but end of query found instead.

Works with both direct strings and variables:

```
$sql = "SELECT * FROM";
$stmt = $db->query($sql);
```

Caution

SQL syntax error in query(): Expected token NAME ~RESERVED, but end of query found instead.

```
// ✅ Valid SQL
$stmt = $db->prepare("SELECT id, name FROM users WHERE id = :id");
```

### 2. Parameter Binding Validation

[](#2-parameter-binding-validation)

Ensures all SQL placeholders have corresponding bindings:

```
// ❌ Missing parameter
$stmt = $db->prepare("SELECT * FROM users WHERE id = :id AND name = :name");
$stmt->execute(['id' => 1]); // Missing :name
```

Caution

Missing parameter :name in execute()

```
// ❌ Extra parameter
$stmt = $db->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute(['id' => 1, 'extra' => 'unused']);
```

Caution

Parameter :extra in execute() is not used

```
// ❌ Wrong parameter name
$stmt = $db->prepare("SELECT * FROM users WHERE id = :user_id");
$stmt->execute(['id' => 1]); // Should be :user_id
```

Caution

Missing parameter :user\_id in execute()

Parameter :id in execute() is not used

```
// ✅ Valid bindings
$stmt = $db->prepare("SELECT * FROM users WHERE id = :id AND name = :name");
$stmt->execute(['id' => 1, 'name' => 'John']);
```

Important: When `execute()` receives an array, it ignores previous `bindValue()` calls:

```
$stmt = $db->prepare("SELECT * FROM users WHERE id = :id");
$stmt->bindValue(':id', 1); // This is ignored!
$stmt->execute(['name' => 'John']); // Wrong parameter
```

Caution

Missing parameter :id in execute()

Parameter :name in execute() is not used

### 3. SELECT Column Validation

[](#3-select-column-validation)

Validates that SELECT columns match the PHPDoc type annotation.

Note

This rule supports `fetch()`, `fetchObject()`, and `fetchAll()` methods, assuming the fetch mode of the database connection is `PDO::FETCH_OBJ` (returning objects). Other fetch modes like `PDO::FETCH_ASSOC` (arrays) or `PDO::FETCH_CLASS` are not currently validated.

```
// ❌ Column typo: "nam" instead of "name"
$stmt = $db->prepare("SELECT id, nam, email FROM users WHERE id = :id");
$stmt->execute(['id' => 1]);

/** @var object{id: int, name: string, email: string} */
$user = $stmt->fetch();
```

Caution

SELECT column mismatch: PHPDoc expects property "name" but SELECT (line X) has "nam" - possible typo?

```
// ❌ Missing column
$stmt = $db->prepare("SELECT id, name FROM users WHERE id = :id");
$stmt->execute(['id' => 1]);

/** @var object{id: int, name: string, email: string} */
$user = $stmt->fetch();
```

Caution

SELECT column missing: PHPDoc expects property "email" but it is not in the SELECT query (line X)

```
// ✅ Valid columns
$stmt = $db->prepare("SELECT id, name, email FROM users WHERE id = :id");
$stmt->execute(['id' => 1]);

/** @var object{id: int, name: string, email: string} */
$user = $stmt->fetch();

// ✅ Also valid - selecting extra columns is fine
$stmt = $db->prepare("SELECT id, name, email, created_at FROM users WHERE id = :id");
$stmt->execute(['id' => 1]);

/** @var object{id: int, name: string, email: string} */
$user = $stmt->fetch(); // No error - extra column `created_at` is ignored
```

Supports `@phpstan-type` aliases:

```
/**
 * @phpstan-type User object{id: int, name: string, email: string}
 */
class UserRepository
{
    public function findUser(int $id): void
    {
        // Typo: "nam" instead of "name", also missing "email"
        $stmt = $this->db->prepare("SELECT id, nam FROM users WHERE id = :id");
        $stmt->execute(['id' => $id]);

        /** @var User */
        $user = $stmt->fetch();
```

Caution

SELECT column mismatch: PHPDoc expects property "name" but SELECT (line X) has "nam" - possible typo?

SELECT column missing: PHPDoc expects property "email" but it is not in the SELECT query (line X)

```
    }
}
```

#### Fetch Method Type Validation

[](#fetch-method-type-validation)

The extension also validates that your PHPDoc type structure matches the fetch method being used:

```
// ❌ fetchAll() returns an array of objects, not a single object
$stmt = $db->prepare("SELECT id, name FROM users");
$stmt->execute();

/** @var object{id: int, name: string} */
$users = $stmt->fetchAll(); // Wrong: should be array type
```

Caution

Type mismatch: fetchAll() returns array&lt;object{...}&gt; but PHPDoc specifies object{...} (line X)

```
// ❌ fetch() returns a single object, not an array
$stmt = $db->prepare("SELECT id, name FROM users WHERE id = :id");
$stmt->execute(['id' => 1]);

/** @var array */
$user = $stmt->fetch(); // Wrong: should be single object type
```

Caution

Type mismatch: fetch() returns object{...} but PHPDoc specifies array&lt;object{...}&gt; (line X)

```
// ✅ Correct: fetchAll() with array type (generic syntax)
$stmt = $db->prepare("SELECT id, name FROM users");
$stmt->execute();

/** @var array */
$users = $stmt->fetchAll();

// ✅ Correct: fetchAll() with array type (suffix syntax)
/** @var object{id: int, name: string}[] */
$users = $stmt->fetchAll();

// ✅ Correct: fetch() with single object type
$stmt = $db->prepare("SELECT id, name FROM users WHERE id = :id");
$stmt->execute(['id' => 1]);

/** @var object{id: int, name: string} */
$user = $stmt->fetch();
```

Note

Both PHPStan array syntaxes are supported:

- Generic syntax: `array`
- Suffix syntax: `object{...}[]`

#### False Return Type Validation

[](#false-return-type-validation)

The extension validates that `fetch()` and `fetchObject()` calls properly handle the `false` return value that occurs when no rows are found.

```
// ❌ Missing |false in type annotation
$stmt = $db->prepare("SELECT id, name FROM users WHERE id = :id");
$stmt->execute(['id' => 1]);

/** @var object{id: int, name: string} */
$user = $stmt->fetch(); // Can return false!
```

Caution

Missing |false in @var type: fetch() can return false when no results found. Either add |false to the type or check for false/rowCount() before using the result (line X)

```
// ✅ Correct: Include |false in union type
$stmt = $db->prepare("SELECT id, name FROM users WHERE id = :id");
$stmt->execute(['id' => 1]);

/** @var object{id: int, name: string}|false */
$user = $stmt->fetch();

// Both styles are supported:
/** @var object{id: int, name: string} | false */  // With spaces
/** @var false|object{id: int, name: string} */    // Reverse order
```

```
// ✅ Correct: Check rowCount() with throw/return
$stmt = $db->prepare("SELECT id, name FROM users WHERE id = :id");
$stmt->execute(['id' => 1]);

if ($stmt->rowCount() === 0) {
    throw new \RuntimeException('User not found');
}

/** @var object{id: int, name: string} */
$user = $stmt->fetch(); // Safe - won't execute if no rows
```

```
// ✅ Correct: Check for false after fetch
$stmt = $db->prepare("SELECT id, name FROM users WHERE id = :id");
$stmt->execute(['id' => 1]);

/** @var object{id: int, name: string} */
$user = $stmt->fetch();

if ($user === false) {
    throw new \RuntimeException('User not found');
}
// Or: if ($user !== false) { ... }
// Or: if (!$user) { ... }
```

```
// ❌ rowCount() without throw/return doesn't help
$stmt = $db->prepare("SELECT id, name FROM users WHERE id = :id");
$stmt->execute(['id' => 1]);

if ($stmt->rowCount() === 0) {
    // Empty block - execution continues!
}

/** @var object{id: int, name: string} */
$user = $stmt->fetch(); // Still can return false!
```

Caution

Missing |false in @var type: fetch() can return false when no results found. Either add |false to the type or check for false/rowCount() before using the result (line X)

Note

This validation applies only to `fetch()` and `fetchObject()`. The `fetchAll()` method returns an empty array instead of false, so it doesn't require `|false` in the type annotation.

### 4. Self-Reference Detection

[](#4-self-reference-detection)

Detects self-reference conditions where the same column is compared to itself. This is likely a bug where the developer meant to reference a different table or column.

```
// ❌ Self-reference in JOIN condition
$stmt = $db->prepare("
    SELECT *
    FROM orders
    INNER JOIN users ON users.id = users.id
");
```

Caution

Self-referencing JOIN condition: 'users.id = users.id'

```
// ❌ Self-reference in WHERE clause
$stmt = $db->prepare("
    SELECT *
    FROM products
    WHERE products.category_id = products.category_id
");
```

Caution

Self-referencing WHERE condition: 'products.category\_id = products.category\_id'

```
// ❌ Multiple self-references in same query
$stmt = $db->prepare("
    SELECT *
    FROM orders
    INNER JOIN products ON products.id = products.id
    WHERE products.active = products.active
");
```

Caution

Self-referencing JOIN condition: 'products.id = products.id'

Self-referencing WHERE condition: 'products.active = products.active'

```
// ✅ Valid JOIN - different columns
$stmt = $db->prepare("
    SELECT *
    FROM orders
    INNER JOIN users ON orders.user_id = users.id
");

// ✅ Valid WHERE - comparing to a value
$stmt = $db->prepare("
    SELECT *
    FROM products
    WHERE products.category_id = 5
");
```

Note

This rule works with:

- `INNER JOIN`, `LEFT JOIN`, `RIGHT JOIN` conditions
- `WHERE` clause conditions (including `AND`/`OR` combinations)
- Both `SELECT` and `INSERT...SELECT` queries
- Queries with PDO placeholders (`:parameter`)

The rule reports errors on the exact line where the self-reference occurs, making it easy to locate and fix the issue.

### 5. Invalid Table Reference Detection

[](#5-invalid-table-reference-detection)

Detects typos in table and alias names used in qualified column references. Catches errors like using `user.name` when the table is `users`, or referencing a table that doesn't appear in FROM/JOIN clauses.

```
// ❌ Table 'user' doesn't exist - should be 'users'
$stmt = $db->prepare("SELECT user.name FROM users WHERE users.id = :id");
```

Caution

Invalid table reference 'user' - available tables/aliases: users

```
// ❌ Wrong alias - using 'usr' but alias is 'u'
$stmt = $db->prepare("SELECT usr.name FROM users AS u WHERE u.id = :id");
```

Caution

Invalid table reference 'usr' - available tables/aliases: u, users

```
// ❌ Table 'orders' not in FROM or JOIN
$stmt = $db->prepare("SELECT users.id, orders.total FROM users WHERE users.id = :id");
```

Caution

Invalid table reference 'orders' - available tables/aliases: users

```
// ✅ Correct table name
$stmt = $db->prepare("SELECT users.name FROM users WHERE users.id = :id");

// ✅ Correct alias usage
$stmt = $db->prepare("SELECT u.name FROM users AS u WHERE u.id = :id");

// ✅ Both table name and alias can be used
$stmt = $db->prepare("SELECT users.id, u.name FROM users AS u WHERE u.id = :id");

// ✅ Multiple tables with JOIN
$stmt = $db->prepare("
    SELECT u.name, o.total
    FROM users AS u
    INNER JOIN orders AS o ON u.id = o.user_id
    WHERE u.id = :id
");
```

The rule validates:

- Column references in SELECT clause
- Column references in WHERE conditions
- Column references in JOIN conditions
- Column references in ORDER BY and GROUP BY clauses
- Column references in HAVING clause

This catches common typos that would only be discovered at runtime, like:

- Singular/plural mistakes (`user` vs `users`)
- Typos in alias names (`usr` vs `usrs`)
- Wrong table references in complex JOINs

### 6. Tautological Condition Detection

[](#6-tautological-condition-detection)

Detects tautological conditions that are always true or always false. These are often left over from development (e.g., `WHERE 1 = 1` used to easily toggle conditions) and should be removed before committing.

```
// ❌ Always-true condition
$stmt = $db->prepare("
    SELECT *
    FROM users
    WHERE 1 = 1
");
```

Caution

Tautological condition in WHERE clause: '1 = 1' (always true)

```
// ❌ Always-false condition
$stmt = $db->prepare("
    SELECT *
    FROM users
    WHERE 1 = 0
");
```

Caution

Tautological condition in WHERE clause: '1 = 0' (always false)

```
// ❌ String literal tautology
$stmt = $db->prepare("SELECT * FROM users WHERE 'yes' = 'yes'");
```

Caution

Tautological condition in WHERE clause: ''yes' = 'yes'' (always true)

```
// ❌ Boolean tautology
$stmt = $db->prepare("SELECT * FROM users WHERE TRUE = FALSE");
```

Caution

Tautological condition in WHERE clause: 'TRUE = FALSE' (always false)

```
// ❌ Tautology in JOIN condition
$stmt = $db->prepare("
    SELECT *
    FROM users
    INNER JOIN orders ON 1 = 1
");
```

Caution

Tautological condition in JOIN clause: '1 = 1' (always true)

```
// ✅ Valid - comparing column to literal
$stmt = $db->prepare("SELECT * FROM users WHERE status = 1");

// ✅ Valid - using parameter
$stmt = $db->prepare("SELECT * FROM users WHERE id = :id");
```

Note

This rule detects:

- Numeric comparisons: `1 = 1`, `0 = 0`, `42 = 42`, `1 = 0`
- String comparisons: `'yes' = 'yes'`, `'a' = 'b'`
- Boolean comparisons: `TRUE = TRUE`, `FALSE = FALSE`, `TRUE = FALSE`
- In WHERE, JOIN ON, and HAVING clauses

### 7. MySQL-Specific Syntax Detection

[](#7-mysql-specific-syntax-detection)

Detects MySQL-specific SQL syntax that has portable ANSI alternatives. This helps maintain database-agnostic code for future migrations to PostgreSQL, SQL Server, or other databases.

```
// ❌ IFNULL is MySQL-specific
$stmt = $db->prepare("SELECT IFNULL(name, 'Unknown') FROM users");
```

Caution

Use COALESCE() instead of IFNULL() for database portability

```
// ❌ IF() is MySQL-specific
$stmt = $db->prepare("SELECT IF(status = 1, 'Active', 'Inactive') FROM users");
```

Caution

Use CASE WHEN instead of IF() for database portability

```
// ✅ COALESCE is portable (works in MySQL, PostgreSQL, SQL Server)
$stmt = $db->prepare("SELECT COALESCE(name, 'Unknown') FROM users");

// ✅ CASE WHEN is portable
$stmt = $db->prepare("SELECT CASE WHEN status = 1 THEN 'Active' ELSE 'Inactive' END FROM users");
```

```
// ❌ NOW() is MySQL-specific
$stmt = $db->prepare("SELECT * FROM users WHERE created_at > NOW()");
```

Caution

Bind current datetime to a PHP variable instead of NOW() for database portability

```
// ❌ CURDATE() is MySQL-specific
$stmt = $db->prepare("SELECT * FROM users WHERE birth_date = CURDATE()");
```

Caution

Bind current date to a PHP variable instead of CURDATE() for database portability

```
// ❌ LIMIT offset, count is MySQL-specific
$stmt = $db->prepare("SELECT * FROM users LIMIT 10, 5");
```

Caution

Use LIMIT count OFFSET offset instead of LIMIT offset, count for database portability

```
// ✅ Bind PHP datetime variables
$stmt = $db->prepare("SELECT * FROM users WHERE created_at > :now");
$stmt->execute(['now' => (new \DateTime())->format('Y-m-d H:i:s')]);

$stmt = $db->prepare("SELECT * FROM users WHERE birth_date = :today");
$stmt->execute(['today' => (new \DateTime())->format('Y-m-d')]);

// ✅ LIMIT count OFFSET offset is portable
$stmt = $db->prepare("SELECT * FROM users LIMIT 5 OFFSET 10");
```

Currently detects:

- `IFNULL()` → Use `COALESCE()`
- `IF()` → Use `CASE WHEN`
- `NOW()` → Bind PHP datetime variable
- `CURDATE()` → Bind PHP date variable
- `LIMIT offset, count` → Use `LIMIT count OFFSET offset`

Requirements
------------

[](#requirements)

- PHP 8.1+
- PHPStan 1.10+
- SQLFTW 0.1+ (SQL syntax validation)

How It Works
------------

[](#how-it-works)

All four rules use a two-pass analysis approach:

1. **First pass**: Scan the method for SQL query strings (both direct literals and variables)
2. **Second pass**: Find all `prepare()`/`query()` calls and validate them

This allows the rules to work with both patterns:

```
// Direct string literals
$stmt = $db->prepare("SELECT ...");

// Variables
$sql = "SELECT ...";
$stmt = $db->prepare($sql);
```

The rules also handle SQL queries prepared in constructors and used in other methods.

Known Limitations
-----------------

[](#known-limitations)

- SQL queries with variable interpolation (e.g., `"SELECT $column FROM table"`) cannot be validated
- `SELECT *` and `SELECT table.*` queries cannot be validated for column matching (no way to know columns statically)
- Very long queries (&gt;10,000 characters) are skipped for performance
- Cross-file SQL tracking is limited to class properties

Performance
-----------

[](#performance)

These rules are designed to be fast:

- Early bailouts for non-SQL code
- Efficient SQL detection heuristics
- Skips very long queries (&gt;10,000 characters)
- Gracefully handles missing dependencies

Available Error Identifiers
---------------------------

[](#available-error-identifiers)

IdentifierRuleDescription`pdoSql.sqlSyntax`SQL Syntax ValidationSQL syntax error detected`pdoSql.missingParameter`Parameter BindingsParameter expected in SQL but missing from `execute()` array`pdoSql.extraParameter`Parameter BindingsParameter in `execute()` array but not used in SQL`pdoSql.missingBinding`Parameter BindingsParameter expected but no `bindValue()`/`bindParam()` found`pdoSql.extraBinding`Parameter BindingsParameter bound but not used in SQL`pdoSql.columnMismatch`SELECT Column ValidationColumn name typo detected (case-sensitive)`pdoSql.columnMissing`SELECT Column ValidationPHPDoc property missing from SELECT`pdoSql.fetchTypeMismatch`SELECT Column ValidationFetch method doesn't match PHPDoc type structure`pdoSql.missingFalseType`SELECT Column ValidationMissing `|false` union type for `fetch()`/`fetchObject()``pdoSql.selfReferenceCondition`Self-Reference DetectionSelf-referencing condition in JOIN or WHERE clause`pdoSql.invalidTableReference`Invalid Table Reference DetectionInvalid table or alias name in qualified column reference`pdoSql.mySqlSpecific`MySQL-Specific SyntaxMySQL-specific function with portable alternative`pdoSql.tautologicalCondition`Tautological Condition DetectionAlways-true or always-false condition detected### Ignoring Specific Errors

[](#ignoring-specific-errors)

All errors from this extension have custom identifiers that allow you to selectively ignore them in your `phpstan.neon`:

```
parameters:
    ignoreErrors:
        # Ignore all SQL syntax errors
        - identifier: pdoSql.sqlSyntax

        # Ignore all parameter binding errors
        - identifier: pdoSql.missingParameter
        - identifier: pdoSql.extraParameter
        - identifier: pdoSql.missingBinding
        - identifier: pdoSql.extraBinding

        # Ignore all SELECT column validation errors
        - identifier: pdoSql.columnMismatch
        - identifier: pdoSql.columnMissing
        - identifier: pdoSql.fetchTypeMismatch
        - identifier: pdoSql.missingFalseType

        # Ignore all self-reference detection errors
        - identifier: pdoSql.selfReferenceCondition

        # Ignore all invalid table reference detection errors
        - identifier: pdoSql.invalidTableReference

        # Ignore all MySQL-specific syntax errors
        - identifier: pdoSql.mySqlSpecific

        # Ignore all tautological condition errors
        - identifier: pdoSql.tautologicalCondition
```

You can also ignore errors by path or message pattern:

```
parameters:
    ignoreErrors:
        # Ignore SQL syntax errors in migration files
        -
            identifier: pdoSql.sqlSyntax
            path: */migrations/*

        # Ignore missing parameter errors for a specific parameter
        -
            message: '#Missing parameter :legacy_id#'
            identifier: pdoSql.missingParameter
```

Playground
----------

[](#playground)

Want to try the extension quickly? Open `playground/example.php` in your IDE with a PHPStan plugin installed. You'll see errors highlighted in real-time as you edit the code.

Developer Tools
---------------

[](#developer-tools)

### `ddt()` - Dump Debug Type

[](#ddt---dump-debug-type)

The `ddt()` helper function inspects PHP values at runtime and generates PHPStan type definitions. This is useful for quickly creating `@phpstan-type` annotations from real data in tests.

**Usage in PHPUnit tests:**

```
use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    public function testExample(): void
    {
        $row = $stmt->fetch(); // Fetch data from database
        ddt($row); // Dumps type and stops execution
    }
}
```

**Terminal output:**

```
/**
 * @phpstan-type Item object{
 *  id: int,
 *  name: string,
 *  status: int,
 * }
 */
```

Simply copy the output and paste it into your code as a type annotation!

**Supported types:**

- **Objects** (stdClass and class instances): Shows public properties as `object{...}` shape
- **Associative arrays**: Formatted as `array{key: type, ...}`
- **Sequential arrays**: Formatted as `array`
- **Nested structures**: Handles nesting up to 5 levels deep
- **All scalar types**: int, float, string, bool, null

**Type mapping:**

PHP Runtime TypePHPStan Output`integer``int``double``float``string``string``boolean``bool``NULL``null``array` (associative)`array{key: type, ...}``array` (sequential)`array``object``object{prop: type, ...}`**Examples:**

```
// Nested objects
$workflow = new stdClass();
$workflow->id = 1;
$workflow->metadata = new stdClass();
$workflow->metadata->created_at = '2024-01-01';

ddt($workflow);

// Output:
/**
 * @phpstan-type Item object{
 *  id: int,
 *  metadata: object{
 *    created_at: string,
 *  },
 * }
 */
```

```
// Associative array
$config = ['database' => 'mysql', 'port' => 3306];
ddt($config);

// Output:
/**
 * @phpstan-type Item array{
 *  database: string,
 *  port: int,
 * }
 */
```

```
// Sequential array
$ids = [1, 2, 3, 4, 5];
ddt($ids);

// Output:
/**
 * @phpstan-type Item array
 */
```

**Note:** The function calls `exit(0)` after dumping (like `dd()`), so execution stops. This is intentional for use in debugging/testing workflows.

### `ddc()` - Dump Debug Class

[](#ddc---dump-debug-class)

The `ddc()` helper function inspects PHP objects at runtime and generates PHP class definitions. This is useful for creating view model classes compatible with `PDO::fetchObject()`.

**Usage in PHPUnit tests:**

```
use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    public function testExample(): void
    {
        $row = $stmt->fetchObject(); // Fetch data from database
        ddc($row); // Dumps class definition and stops execution
    }
}
```

**Terminal output:**

```
class Item
{
    public int $id;
    public string $name;
    public string $email;
    public ?string $phone;
}
```

Simply copy the output, rename the class, and use it as your view model!

**Example workflow:**

```
// 1. First, discover the structure using ddc()
$stmt = $db->query("SELECT id, name, email, phone FROM users WHERE id = 1");
$row = $stmt->fetchObject();
ddc($row);

// 2. Create your view model class from the output
class UserViewModel
{
    public int $id;
    public string $name;
    public string $email;
    public ?string $phone;
}

// 3. Use it with PDO::fetchObject()
$stmt = $db->query("SELECT id, name, email, phone FROM users WHERE id = 1");
$user = $stmt->fetchObject(UserViewModel::class);
```

**Supported types:**

PHP Runtime ValueGenerated Type`integer``int``double``float``string``string``boolean``bool``NULL``mixed``array``array``object``object`**Note:** Like `ddt()`, this function calls `exit(0)` after dumping.

Development
-----------

[](#development)

To contribute to this project:

1. Clone the repository:

```
git clone https://github.com/pierresh/phpstan-pdo-mysql.git
cd phpstan-pdo-mysql
```

2. Install dependencies:

```
composer install
```

3. Run tests:

```
composer test
```

This will start PHPUnit watcher that automatically runs tests when files change.

To run tests once without watching:

```
./vendor/bin/phpunit
```

4. Analyze source code with PHPStan:

```
composer analyze
```

This analyzes only the `./src` directory (excludes playground and test fixtures) at maximum level.

5. Refactor code with Rector:

```
composer refactor:dry  # Preview changes without applying
composer refactor      # Apply refactoring changes
```

Rector is configured to modernize code to PHP 8.1+ standards with code quality improvements.

6. Format code with Mago:

```
composer format:check  # Check formatting without making changes
composer format        # Apply code formatting
```

Mago provides consistent, opinionated code formatting for PHP 8.1+.

7. Lint code with Mago:

```
composer lint          # Run Mago linter
```

8. Analyze code with Mago:

```
composer mago:analyze  # Run Mago static analyzer
```

Mago's analyzer provides fast, type-level analysis to find logical errors and type mismatches.

License
-------

[](#license)

MIT

Contributing
------------

[](#contributing)

Contributions welcome! Please open an issue or submit a pull request.

###  Health Score

45

—

FairBetter than 92% of packages

Maintenance96

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity54

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~5 days

Recently: every ~11 days

Total

26

Last Release

48d ago

Major Versions

1.0.25 → 2.0.02026-03-22

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/7520095?v=4)[Pierre](/maintainers/pierresh)[@pierresh](https://github.com/pierresh)

---

Tags

mysqlpdophpstanphpstan-extensionsqlstatic-analysisPHPStanstatic analysismysqlpdosql-validation

###  Code Quality

TestsPHPUnit

Static AnalysisRector

Type Coverage Yes

### Embed Badge

![Health badge](/badges/pierresh-phpstan-pdo-mysql/health.svg)

```
[![Health](https://phpackages.com/badges/pierresh-phpstan-pdo-mysql/health.svg)](https://phpackages.com/packages/pierresh-phpstan-pdo-mysql)
```

###  Alternatives

[doctrine/dbal

Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.

9.7k578.4M5.6k](/packages/doctrine-dbal)[ifsnop/mysqldump-php

PHP version of mysqldump cli that comes with MySQL

1.3k5.5M68](/packages/ifsnop-mysqldump-php)[nette/database

💾 Nette Database: layer with a familiar PDO-like API but much more powerful. Building queries, advanced joins, drivers for MySQL, PostgreSQL, SQLite, MS SQL Server and Oracle.

5656.7M231](/packages/nette-database)[dibi/dibi

Dibi is Database Abstraction Library for PHP

5013.8M120](/packages/dibi-dibi)[aura/sql

A PDO extension that provides lazy connections, array quoting, query profiling, value binding, and convenience methods for common fetch styles. Because it extends PDO, existing code that uses PDO can use this without any changes to the existing code.

5632.5M43](/packages/aura-sql)[aura/sqlquery

Object-oriented query builders for MySQL, Postgres, SQLite, and SQLServer; can be used with any database connection library.

4572.9M34](/packages/aura-sqlquery)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
