https://github.com/pierresh/phpstan-pdo-mysql
Static analysis for SQL queries in PHP code
https://github.com/pierresh/phpstan-pdo-mysql
mysql pdo phpstan phpstan-extension sql static-analysis
Last synced: about 1 month ago
JSON representation
Static analysis for SQL queries in PHP code
- Host: GitHub
- URL: https://github.com/pierresh/phpstan-pdo-mysql
- Owner: pierresh
- License: mit
- Created: 2025-11-01T12:49:21.000Z (6 months ago)
- Default Branch: master
- Last Pushed: 2026-01-15T09:47:15.000Z (4 months ago)
- Last Synced: 2026-01-17T04:48:11.194Z (4 months ago)
- Topics: mysql, pdo, phpstan, phpstan-extension, sql, static-analysis
- Language: PHP
- Homepage: https://packagist.org/packages/pierresh/phpstan-pdo-mysql
- Size: 198 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# 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
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
```bash
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`:
```neon
includes:
- vendor/pierresh/phpstan-pdo-mysql/extension.neon
```
## Examples
### 1. SQL Syntax Validation
Catches syntax errors in SQL queries:
```php
// ❌ 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:
```php
$sql = "SELECT * FROM";
$stmt = $db->query($sql);
```
> [!CAUTION]
> SQL syntax error in query(): Expected token NAME ~RESERVED, but end of query found instead.
```php
// ✅ Valid SQL
$stmt = $db->prepare("SELECT id, name FROM users WHERE id = :id");
```
### 2. Parameter Binding Validation
Ensures all SQL placeholders have corresponding bindings:
```php
// ❌ 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()
```php
// ❌ 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
```php
// ❌ 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
```php
// ✅ 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:
```php
$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
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.
```php
// ❌ 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?
```php
// ❌ 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)
```php
// ✅ 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:
```php
/**
* @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)
```php
}
}
```
#### Fetch Method Type Validation
The extension also validates that your PHPDoc type structure matches the fetch method being used:
```php
// ❌ 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 but PHPDoc specifies object{...} (line X)
```php
// ❌ 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 (line X)
```php
// ✅ 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
The extension validates that `fetch()` and `fetchObject()` calls properly handle the `false` return value that occurs when no rows are found.
```php
// ❌ 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)
```php
// ✅ 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
```
```php
// ✅ 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
```
```php
// ✅ 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) { ... }
```
```php
// ❌ 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
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.
```php
// ❌ 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'
```php
// ❌ 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'
```php
// ❌ 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'
```php
// ✅ 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
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.
```php
// ❌ 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
```php
// ❌ 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
```php
// ❌ 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
```php
// ✅ 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
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.
```php
// ❌ Always-true condition
$stmt = $db->prepare("
SELECT *
FROM users
WHERE 1 = 1
");
```
> [!CAUTION]
> Tautological condition in WHERE clause: '1 = 1' (always true)
```php
// ❌ Always-false condition
$stmt = $db->prepare("
SELECT *
FROM users
WHERE 1 = 0
");
```
> [!CAUTION]
> Tautological condition in WHERE clause: '1 = 0' (always false)
```php
// ❌ String literal tautology
$stmt = $db->prepare("SELECT * FROM users WHERE 'yes' = 'yes'");
```
> [!CAUTION]
> Tautological condition in WHERE clause: ''yes' = 'yes'' (always true)
```php
// ❌ Boolean tautology
$stmt = $db->prepare("SELECT * FROM users WHERE TRUE = FALSE");
```
> [!CAUTION]
> Tautological condition in WHERE clause: 'TRUE = FALSE' (always false)
```php
// ❌ 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)
```php
// ✅ 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
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.
```php
// ❌ IFNULL is MySQL-specific
$stmt = $db->prepare("SELECT IFNULL(name, 'Unknown') FROM users");
```
> [!CAUTION]
> Use COALESCE() instead of IFNULL() for database portability
```php
// ❌ 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
```php
// ✅ 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");
```
```php
// ❌ 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
```php
// ❌ 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
```php
// ❌ 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
```php
// ✅ 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
- PHP 8.1+
- PHPStan 1.10+
- SQLFTW 0.1+ (SQL syntax validation)
## 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:
```php
// 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
- 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 (>10,000 characters) are skipped for performance
- Cross-file SQL tracking is limited to class properties
## Performance
These rules are designed to be fast:
- Early bailouts for non-SQL code
- Efficient SQL detection heuristics
- Skips very long queries (>10,000 characters)
- Gracefully handles missing dependencies
## Available Error Identifiers
| Identifier | Rule | Description |
|------------|------|-------------|
| `pdoSql.sqlSyntax` | SQL Syntax Validation | SQL syntax error detected |
| `pdoSql.missingParameter` | Parameter Bindings | Parameter expected in SQL but missing from `execute()` array |
| `pdoSql.extraParameter` | Parameter Bindings | Parameter in `execute()` array but not used in SQL |
| `pdoSql.missingBinding` | Parameter Bindings | Parameter expected but no `bindValue()`/`bindParam()` found |
| `pdoSql.extraBinding` | Parameter Bindings | Parameter bound but not used in SQL |
| `pdoSql.columnMismatch` | SELECT Column Validation | Column name typo detected (case-sensitive) |
| `pdoSql.columnMissing` | SELECT Column Validation | PHPDoc property missing from SELECT |
| `pdoSql.fetchTypeMismatch` | SELECT Column Validation | Fetch method doesn't match PHPDoc type structure |
| `pdoSql.missingFalseType` | SELECT Column Validation | Missing `\|false` union type for `fetch()`/`fetchObject()` |
| `pdoSql.selfReferenceCondition` | Self-Reference Detection | Self-referencing condition in JOIN or WHERE clause |
| `pdoSql.invalidTableReference` | Invalid Table Reference Detection | Invalid table or alias name in qualified column reference |
| `pdoSql.mySqlSpecific` | MySQL-Specific Syntax | MySQL-specific function with portable alternative |
| `pdoSql.tautologicalCondition` | Tautological Condition Detection | Always-true or always-false condition detected |
### Ignoring Specific Errors
All errors from this extension have custom identifiers that allow you to selectively ignore them in your `phpstan.neon`:
```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:
```neon
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
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
### `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:**
```php
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:**
```php
/**
* @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 Type | PHPStan Output |
|-----------------|----------------|
| `integer` | `int` |
| `double` | `float` |
| `string` | `string` |
| `boolean` | `bool` |
| `NULL` | `null` |
| `array` (associative) | `array{key: type, ...}` |
| `array` (sequential) | `array` |
| `object` | `object{prop: type, ...}` |
**Examples:**
```php
// 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,
* },
* }
*/
```
```php
// Associative array
$config = ['database' => 'mysql', 'port' => 3306];
ddt($config);
// Output:
/**
* @phpstan-type Item array{
* database: string,
* port: int,
* }
*/
```
```php
// 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
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:**
```php
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:**
```php
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:**
```php
// 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 Value | Generated 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
To contribute to this project:
1. Clone the repository:
```bash
git clone https://github.com/pierresh/phpstan-pdo-mysql.git
cd phpstan-pdo-mysql
```
2. Install dependencies:
```bash
composer install
```
3. Run tests:
```bash
composer test
```
This will start PHPUnit watcher that automatically runs tests when files change.
To run tests once without watching:
```bash
./vendor/bin/phpunit
```
4. Analyze source code with PHPStan:
```bash
composer analyze
```
This analyzes only the `./src` directory (excludes playground and test fixtures) at maximum level.
5. Refactor code with Rector:
```bash
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:
```bash
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:
```bash
composer lint # Run Mago linter
```
8. Analyze code with Mago:
```bash
composer mago:analyze # Run Mago static analyzer
```
Mago's analyzer provides fast, type-level analysis to find logical errors and type mismatches.
## License
MIT
## Contributing
Contributions welcome! Please open an issue or submit a pull request.