{"id":33065257,"url":"https://github.com/pierresh/phpstan-pdo-mysql","last_synced_at":"2026-04-01T22:56:55.415Z","repository":{"id":321941800,"uuid":"1087690649","full_name":"pierresh/phpstan-pdo-mysql","owner":"pierresh","description":"Static analysis for SQL queries in PHP code","archived":false,"fork":false,"pushed_at":"2026-01-15T09:47:15.000Z","size":203,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-01-17T04:48:11.194Z","etag":null,"topics":["mysql","pdo","phpstan","phpstan-extension","sql","static-analysis"],"latest_commit_sha":null,"homepage":"https://packagist.org/packages/pierresh/phpstan-pdo-mysql","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pierresh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-11-01T12:49:21.000Z","updated_at":"2026-01-07T03:37:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/pierresh/phpstan-pdo-mysql","commit_stats":null,"previous_names":["pierresh/phpstan-pdo-mysql"],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/pierresh/phpstan-pdo-mysql","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pierresh%2Fphpstan-pdo-mysql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pierresh%2Fphpstan-pdo-mysql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pierresh%2Fphpstan-pdo-mysql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pierresh%2Fphpstan-pdo-mysql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pierresh","download_url":"https://codeload.github.com/pierresh/phpstan-pdo-mysql/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pierresh%2Fphpstan-pdo-mysql/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29152108,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-06T02:39:25.012Z","status":"ssl_error","status_checked_at":"2026-02-06T02:37:22.784Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["mysql","pdo","phpstan","phpstan-extension","sql","static-analysis"],"created_at":"2025-11-14T07:02:48.114Z","updated_at":"2026-02-06T05:16:48.379Z","avatar_url":"https://github.com/pierresh.png","language":"PHP","readme":"# PHPStan PDO MySQL Rules\n\nStatic analysis rules for PHPStan that validate PDO/MySQL code for common errors that would otherwise only be caught at runtime.\n\n## Features\n\nThis extension provides seven powerful rules that work without requiring a database connection:\n\n1. **SQL Syntax Validation** - Detects MySQL syntax errors in `prepare()` and `query()` calls\n2. **Parameter Binding Validation** - Ensures PDO parameters match SQL placeholders\n3. **SELECT Column Validation** - Verifies SELECT columns match PHPDoc type annotations\n4. **Self-Reference Detection** - Catches self-reference conditions in JOIN and WHERE clauses\n5. **Invalid Table Reference Detection** - Catches typos in table/alias names (e.g., `user.name` when table is `users`)\n6. **Tautological Condition Detection** - Catches always-true/false conditions like `WHERE 1 = 1`\n7. **MySQL-Specific Syntax Detection** - Flags MySQL-specific functions that have portable ANSI alternatives\n\nAll validation is performed statically by analyzing your code, so no database setup is needed.\n\n**Developer Tools:**\n- **`ddt()` Helper Function** - Generates PHPStan type definitions from runtime values for easy copy-paste into your code\n- **`ddc()` Helper Function** - Generates PHP class definitions from objects for use with `PDO::fetchObject()`\n\n## Installation\n\n```bash\ncomposer require --dev pierresh/phpstan-pdo-mysql\n```\n\nThe extension will be automatically registered if you use [phpstan/extension-installer](https://github.com/phpstan/extension-installer).\n\nManual registration in `phpstan.neon`:\n\n```neon\nincludes:\n    - vendor/pierresh/phpstan-pdo-mysql/extension.neon\n```\n\n## Examples\n\n### 1. SQL Syntax Validation\n\nCatches syntax errors in SQL queries:\n\n```php\n// ❌ Incomplete query\n$stmt = $db-\u003equery(\"SELECT * FROM\");\n```\n\n\u003e [!CAUTION]\n\u003e SQL syntax error in query(): Expected token NAME ~RESERVED, but end of query found instead.\n\nWorks with both direct strings and variables:\n\n```php\n$sql = \"SELECT * FROM\";\n$stmt = $db-\u003equery($sql);\n```\n\n\u003e [!CAUTION]\n\u003e SQL syntax error in query(): Expected token NAME ~RESERVED, but end of query found instead.\n\n```php\n// ✅ Valid SQL\n$stmt = $db-\u003eprepare(\"SELECT id, name FROM users WHERE id = :id\");\n```\n\n### 2. Parameter Binding Validation\n\nEnsures all SQL placeholders have corresponding bindings:\n\n```php\n// ❌ Missing parameter\n$stmt = $db-\u003eprepare(\"SELECT * FROM users WHERE id = :id AND name = :name\");\n$stmt-\u003eexecute(['id' =\u003e 1]); // Missing :name\n```\n\n\u003e [!CAUTION]\n\u003e Missing parameter :name in execute()\n\n```php\n// ❌ Extra parameter\n$stmt = $db-\u003eprepare(\"SELECT * FROM users WHERE id = :id\");\n$stmt-\u003eexecute(['id' =\u003e 1, 'extra' =\u003e 'unused']);\n```\n\n\u003e [!CAUTION]\n\u003e Parameter :extra in execute() is not used\n\n```php\n// ❌ Wrong parameter name\n$stmt = $db-\u003eprepare(\"SELECT * FROM users WHERE id = :user_id\");\n$stmt-\u003eexecute(['id' =\u003e 1]); // Should be :user_id\n```\n\n\u003e [!CAUTION]\n\u003e Missing parameter :user_id in execute()\n\u003e\n\u003e Parameter :id in execute() is not used\n\n```php\n// ✅ Valid bindings\n$stmt = $db-\u003eprepare(\"SELECT * FROM users WHERE id = :id AND name = :name\");\n$stmt-\u003eexecute(['id' =\u003e 1, 'name' =\u003e 'John']);\n```\n\nImportant: When `execute()` receives an array, it ignores previous `bindValue()` calls:\n\n```php\n$stmt = $db-\u003eprepare(\"SELECT * FROM users WHERE id = :id\");\n$stmt-\u003ebindValue(':id', 1); // This is ignored!\n$stmt-\u003eexecute(['name' =\u003e 'John']); // Wrong parameter\n```\n\n\u003e [!CAUTION]\n\u003e Missing parameter :id in execute()\n\u003e\n\u003e Parameter :name in execute() is not used\n\n### 3. SELECT Column Validation\n\nValidates that SELECT columns match the PHPDoc type annotation.\n\n\u003e [!NOTE]\n\u003e 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.\n\n```php\n// ❌ Column typo: \"nam\" instead of \"name\"\n$stmt = $db-\u003eprepare(\"SELECT id, nam, email FROM users WHERE id = :id\");\n$stmt-\u003eexecute(['id' =\u003e 1]);\n\n/** @var object{id: int, name: string, email: string} */\n$user = $stmt-\u003efetch();\n```\n\n\u003e [!CAUTION]\n\u003e SELECT column mismatch: PHPDoc expects property \"name\" but SELECT (line X) has \"nam\" - possible typo?\n\n```php\n// ❌ Missing column\n$stmt = $db-\u003eprepare(\"SELECT id, name FROM users WHERE id = :id\");\n$stmt-\u003eexecute(['id' =\u003e 1]);\n\n/** @var object{id: int, name: string, email: string} */\n$user = $stmt-\u003efetch();\n```\n\n\u003e [!CAUTION]\n\u003e SELECT column missing: PHPDoc expects property \"email\" but it is not in the SELECT query (line X)\n\n```php\n// ✅ Valid columns\n$stmt = $db-\u003eprepare(\"SELECT id, name, email FROM users WHERE id = :id\");\n$stmt-\u003eexecute(['id' =\u003e 1]);\n\n/** @var object{id: int, name: string, email: string} */\n$user = $stmt-\u003efetch();\n\n// ✅ Also valid - selecting extra columns is fine\n$stmt = $db-\u003eprepare(\"SELECT id, name, email, created_at FROM users WHERE id = :id\");\n$stmt-\u003eexecute(['id' =\u003e 1]);\n\n/** @var object{id: int, name: string, email: string} */\n$user = $stmt-\u003efetch(); // No error - extra column `created_at` is ignored\n```\n\nSupports `@phpstan-type` aliases:\n\n```php\n/**\n * @phpstan-type User object{id: int, name: string, email: string}\n */\nclass UserRepository\n{\n    public function findUser(int $id): void\n    {\n        // Typo: \"nam\" instead of \"name\", also missing \"email\"\n        $stmt = $this-\u003edb-\u003eprepare(\"SELECT id, nam FROM users WHERE id = :id\");\n        $stmt-\u003eexecute(['id' =\u003e $id]);\n\n        /** @var User */\n        $user = $stmt-\u003efetch();\n```\n\n\u003e [!CAUTION]\n\u003e SELECT column mismatch: PHPDoc expects property \"name\" but SELECT (line X) has \"nam\" - possible typo?\n\u003e\n\u003e SELECT column missing: PHPDoc expects property \"email\" but it is not in the SELECT query (line X)\n\n```php\n    }\n}\n```\n\n#### Fetch Method Type Validation\n\nThe extension also validates that your PHPDoc type structure matches the fetch method being used:\n\n```php\n// ❌ fetchAll() returns an array of objects, not a single object\n$stmt = $db-\u003eprepare(\"SELECT id, name FROM users\");\n$stmt-\u003eexecute();\n\n/** @var object{id: int, name: string} */\n$users = $stmt-\u003efetchAll(); // Wrong: should be array type\n```\n\n\u003e [!CAUTION]\n\u003e Type mismatch: fetchAll() returns array\u003cobject{...}\u003e but PHPDoc specifies object{...} (line X)\n\n```php\n// ❌ fetch() returns a single object, not an array\n$stmt = $db-\u003eprepare(\"SELECT id, name FROM users WHERE id = :id\");\n$stmt-\u003eexecute(['id' =\u003e 1]);\n\n/** @var array\u003cobject{id: int, name: string}\u003e */\n$user = $stmt-\u003efetch(); // Wrong: should be single object type\n```\n\n\u003e [!CAUTION]\n\u003e Type mismatch: fetch() returns object{...} but PHPDoc specifies array\u003cobject{...}\u003e (line X)\n\n```php\n// ✅ Correct: fetchAll() with array type (generic syntax)\n$stmt = $db-\u003eprepare(\"SELECT id, name FROM users\");\n$stmt-\u003eexecute();\n\n/** @var array\u003cobject{id: int, name: string}\u003e */\n$users = $stmt-\u003efetchAll();\n\n// ✅ Correct: fetchAll() with array type (suffix syntax)\n/** @var object{id: int, name: string}[] */\n$users = $stmt-\u003efetchAll();\n\n// ✅ Correct: fetch() with single object type\n$stmt = $db-\u003eprepare(\"SELECT id, name FROM users WHERE id = :id\");\n$stmt-\u003eexecute(['id' =\u003e 1]);\n\n/** @var object{id: int, name: string} */\n$user = $stmt-\u003efetch();\n```\n\n\u003e [!NOTE]\n\u003e Both PHPStan array syntaxes are supported:\n\u003e - Generic syntax: `array\u003cobject{...}\u003e`\n\u003e - Suffix syntax: `object{...}[]`\n\n#### False Return Type Validation\n\nThe extension validates that `fetch()` and `fetchObject()` calls properly handle the `false` return value that occurs when no rows are found.\n\n```php\n// ❌ Missing |false in type annotation\n$stmt = $db-\u003eprepare(\"SELECT id, name FROM users WHERE id = :id\");\n$stmt-\u003eexecute(['id' =\u003e 1]);\n\n/** @var object{id: int, name: string} */\n$user = $stmt-\u003efetch(); // Can return false!\n```\n\n\u003e [!CAUTION]\n\u003e 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)\n\n```php\n// ✅ Correct: Include |false in union type\n$stmt = $db-\u003eprepare(\"SELECT id, name FROM users WHERE id = :id\");\n$stmt-\u003eexecute(['id' =\u003e 1]);\n\n/** @var object{id: int, name: string}|false */\n$user = $stmt-\u003efetch();\n\n// Both styles are supported:\n/** @var object{id: int, name: string} | false */  // With spaces\n/** @var false|object{id: int, name: string} */    // Reverse order\n```\n\n```php\n// ✅ Correct: Check rowCount() with throw/return\n$stmt = $db-\u003eprepare(\"SELECT id, name FROM users WHERE id = :id\");\n$stmt-\u003eexecute(['id' =\u003e 1]);\n\nif ($stmt-\u003erowCount() === 0) {\n    throw new \\RuntimeException('User not found');\n}\n\n/** @var object{id: int, name: string} */\n$user = $stmt-\u003efetch(); // Safe - won't execute if no rows\n```\n\n```php\n// ✅ Correct: Check for false after fetch\n$stmt = $db-\u003eprepare(\"SELECT id, name FROM users WHERE id = :id\");\n$stmt-\u003eexecute(['id' =\u003e 1]);\n\n/** @var object{id: int, name: string} */\n$user = $stmt-\u003efetch();\n\nif ($user === false) {\n    throw new \\RuntimeException('User not found');\n}\n// Or: if ($user !== false) { ... }\n// Or: if (!$user) { ... }\n```\n\n```php\n// ❌ rowCount() without throw/return doesn't help\n$stmt = $db-\u003eprepare(\"SELECT id, name FROM users WHERE id = :id\");\n$stmt-\u003eexecute(['id' =\u003e 1]);\n\nif ($stmt-\u003erowCount() === 0) {\n    // Empty block - execution continues!\n}\n\n/** @var object{id: int, name: string} */\n$user = $stmt-\u003efetch(); // Still can return false!\n```\n\n\u003e [!CAUTION]\n\u003e 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)\n\n\u003e [!NOTE]\n\u003e 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.\n\n### 4. Self-Reference Detection\n\nDetects 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.\n\n```php\n// ❌ Self-reference in JOIN condition\n$stmt = $db-\u003eprepare(\"\n    SELECT *\n    FROM orders\n    INNER JOIN users ON users.id = users.id\n\");\n```\n\n\u003e [!CAUTION]\n\u003e Self-referencing JOIN condition: 'users.id = users.id'\n\n```php\n// ❌ Self-reference in WHERE clause\n$stmt = $db-\u003eprepare(\"\n    SELECT *\n    FROM products\n    WHERE products.category_id = products.category_id\n\");\n```\n\n\u003e [!CAUTION]\n\u003e Self-referencing WHERE condition: 'products.category_id = products.category_id'\n\n```php\n// ❌ Multiple self-references in same query\n$stmt = $db-\u003eprepare(\"\n    SELECT *\n    FROM orders\n    INNER JOIN products ON products.id = products.id\n    WHERE products.active = products.active\n\");\n```\n\n\u003e [!CAUTION]\n\u003e Self-referencing JOIN condition: 'products.id = products.id'\n\u003e\n\u003e Self-referencing WHERE condition: 'products.active = products.active'\n\n```php\n// ✅ Valid JOIN - different columns\n$stmt = $db-\u003eprepare(\"\n    SELECT *\n    FROM orders\n    INNER JOIN users ON orders.user_id = users.id\n\");\n\n// ✅ Valid WHERE - comparing to a value\n$stmt = $db-\u003eprepare(\"\n    SELECT *\n    FROM products\n    WHERE products.category_id = 5\n\");\n```\n\n\u003e [!NOTE]\n\u003e This rule works with:\n\u003e - `INNER JOIN`, `LEFT JOIN`, `RIGHT JOIN` conditions\n\u003e - `WHERE` clause conditions (including `AND`/`OR` combinations)\n\u003e - Both `SELECT` and `INSERT...SELECT` queries\n\u003e - Queries with PDO placeholders (`:parameter`)\n\nThe rule reports errors on the exact line where the self-reference occurs, making it easy to locate and fix the issue.\n\n### 5. Invalid Table Reference Detection\n\nDetects 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.\n\n```php\n// ❌ Table 'user' doesn't exist - should be 'users'\n$stmt = $db-\u003eprepare(\"SELECT user.name FROM users WHERE users.id = :id\");\n```\n\n\u003e [!CAUTION]\n\u003e Invalid table reference 'user' - available tables/aliases: users\n\n```php\n// ❌ Wrong alias - using 'usr' but alias is 'u'\n$stmt = $db-\u003eprepare(\"SELECT usr.name FROM users AS u WHERE u.id = :id\");\n```\n\n\u003e [!CAUTION]\n\u003e Invalid table reference 'usr' - available tables/aliases: u, users\n\n```php\n// ❌ Table 'orders' not in FROM or JOIN\n$stmt = $db-\u003eprepare(\"SELECT users.id, orders.total FROM users WHERE users.id = :id\");\n```\n\n\u003e [!CAUTION]\n\u003e Invalid table reference 'orders' - available tables/aliases: users\n\n```php\n// ✅ Correct table name\n$stmt = $db-\u003eprepare(\"SELECT users.name FROM users WHERE users.id = :id\");\n\n// ✅ Correct alias usage\n$stmt = $db-\u003eprepare(\"SELECT u.name FROM users AS u WHERE u.id = :id\");\n\n// ✅ Both table name and alias can be used\n$stmt = $db-\u003eprepare(\"SELECT users.id, u.name FROM users AS u WHERE u.id = :id\");\n\n// ✅ Multiple tables with JOIN\n$stmt = $db-\u003eprepare(\"\n    SELECT u.name, o.total\n    FROM users AS u\n    INNER JOIN orders AS o ON u.id = o.user_id\n    WHERE u.id = :id\n\");\n```\n\nThe rule validates:\n- Column references in SELECT clause\n- Column references in WHERE conditions\n- Column references in JOIN conditions\n- Column references in ORDER BY and GROUP BY clauses\n- Column references in HAVING clause\n\nThis catches common typos that would only be discovered at runtime, like:\n- Singular/plural mistakes (`user` vs `users`)\n- Typos in alias names (`usr` vs `usrs`)\n- Wrong table references in complex JOINs\n\n### 6. Tautological Condition Detection\n\nDetects 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.\n\n```php\n// ❌ Always-true condition\n$stmt = $db-\u003eprepare(\"\n    SELECT *\n    FROM users\n    WHERE 1 = 1\n\");\n```\n\n\u003e [!CAUTION]\n\u003e Tautological condition in WHERE clause: '1 = 1' (always true)\n\n```php\n// ❌ Always-false condition\n$stmt = $db-\u003eprepare(\"\n    SELECT *\n    FROM users\n    WHERE 1 = 0\n\");\n```\n\n\u003e [!CAUTION]\n\u003e Tautological condition in WHERE clause: '1 = 0' (always false)\n\n```php\n// ❌ String literal tautology\n$stmt = $db-\u003eprepare(\"SELECT * FROM users WHERE 'yes' = 'yes'\");\n```\n\n\u003e [!CAUTION]\n\u003e Tautological condition in WHERE clause: ''yes' = 'yes'' (always true)\n\n```php\n// ❌ Boolean tautology\n$stmt = $db-\u003eprepare(\"SELECT * FROM users WHERE TRUE = FALSE\");\n```\n\n\u003e [!CAUTION]\n\u003e Tautological condition in WHERE clause: 'TRUE = FALSE' (always false)\n\n```php\n// ❌ Tautology in JOIN condition\n$stmt = $db-\u003eprepare(\"\n    SELECT *\n    FROM users\n    INNER JOIN orders ON 1 = 1\n\");\n```\n\n\u003e [!CAUTION]\n\u003e Tautological condition in JOIN clause: '1 = 1' (always true)\n\n```php\n// ✅ Valid - comparing column to literal\n$stmt = $db-\u003eprepare(\"SELECT * FROM users WHERE status = 1\");\n\n// ✅ Valid - using parameter\n$stmt = $db-\u003eprepare(\"SELECT * FROM users WHERE id = :id\");\n```\n\n\u003e [!NOTE]\n\u003e This rule detects:\n\u003e - Numeric comparisons: `1 = 1`, `0 = 0`, `42 = 42`, `1 = 0`\n\u003e - String comparisons: `'yes' = 'yes'`, `'a' = 'b'`\n\u003e - Boolean comparisons: `TRUE = TRUE`, `FALSE = FALSE`, `TRUE = FALSE`\n\u003e - In WHERE, JOIN ON, and HAVING clauses\n\n### 7. MySQL-Specific Syntax Detection\n\nDetects 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.\n\n```php\n// ❌ IFNULL is MySQL-specific\n$stmt = $db-\u003eprepare(\"SELECT IFNULL(name, 'Unknown') FROM users\");\n```\n\n\u003e [!CAUTION]\n\u003e Use COALESCE() instead of IFNULL() for database portability\n\n```php\n// ❌ IF() is MySQL-specific\n$stmt = $db-\u003eprepare(\"SELECT IF(status = 1, 'Active', 'Inactive') FROM users\");\n```\n\n\u003e [!CAUTION]\n\u003e Use CASE WHEN instead of IF() for database portability\n\n```php\n// ✅ COALESCE is portable (works in MySQL, PostgreSQL, SQL Server)\n$stmt = $db-\u003eprepare(\"SELECT COALESCE(name, 'Unknown') FROM users\");\n\n// ✅ CASE WHEN is portable\n$stmt = $db-\u003eprepare(\"SELECT CASE WHEN status = 1 THEN 'Active' ELSE 'Inactive' END FROM users\");\n```\n\n```php\n// ❌ NOW() is MySQL-specific\n$stmt = $db-\u003eprepare(\"SELECT * FROM users WHERE created_at \u003e NOW()\");\n```\n\n\u003e [!CAUTION]\n\u003e Bind current datetime to a PHP variable instead of NOW() for database portability\n\n```php\n// ❌ CURDATE() is MySQL-specific\n$stmt = $db-\u003eprepare(\"SELECT * FROM users WHERE birth_date = CURDATE()\");\n```\n\n\u003e [!CAUTION]\n\u003e Bind current date to a PHP variable instead of CURDATE() for database portability\n\n```php\n// ❌ LIMIT offset, count is MySQL-specific\n$stmt = $db-\u003eprepare(\"SELECT * FROM users LIMIT 10, 5\");\n```\n\n\u003e [!CAUTION]\n\u003e Use LIMIT count OFFSET offset instead of LIMIT offset, count for database portability\n\n```php\n// ✅ Bind PHP datetime variables\n$stmt = $db-\u003eprepare(\"SELECT * FROM users WHERE created_at \u003e :now\");\n$stmt-\u003eexecute(['now' =\u003e (new \\DateTime())-\u003eformat('Y-m-d H:i:s')]);\n\n$stmt = $db-\u003eprepare(\"SELECT * FROM users WHERE birth_date = :today\");\n$stmt-\u003eexecute(['today' =\u003e (new \\DateTime())-\u003eformat('Y-m-d')]);\n\n// ✅ LIMIT count OFFSET offset is portable\n$stmt = $db-\u003eprepare(\"SELECT * FROM users LIMIT 5 OFFSET 10\");\n```\n\nCurrently detects:\n- `IFNULL()` → Use `COALESCE()`\n- `IF()` → Use `CASE WHEN`\n- `NOW()` → Bind PHP datetime variable\n- `CURDATE()` → Bind PHP date variable\n- `LIMIT offset, count` → Use `LIMIT count OFFSET offset`\n\n## Requirements\n\n- PHP 8.1+\n- PHPStan 1.10+\n- SQLFTW 0.1+ (SQL syntax validation)\n\n## How It Works\n\nAll four rules use a two-pass analysis approach:\n\n1. **First pass**: Scan the method for SQL query strings (both direct literals and variables)\n2. **Second pass**: Find all `prepare()`/`query()` calls and validate them\n\nThis allows the rules to work with both patterns:\n\n```php\n// Direct string literals\n$stmt = $db-\u003eprepare(\"SELECT ...\");\n\n// Variables\n$sql = \"SELECT ...\";\n$stmt = $db-\u003eprepare($sql);\n```\n\nThe rules also handle SQL queries prepared in constructors and used in other methods.\n\n## Known Limitations\n\n- SQL queries with variable interpolation (e.g., `\"SELECT $column FROM table\"`) cannot be validated\n- `SELECT *` and `SELECT table.*` queries cannot be validated for column matching (no way to know columns statically)\n- Very long queries (\u003e10,000 characters) are skipped for performance\n- Cross-file SQL tracking is limited to class properties\n\n## Performance\n\nThese rules are designed to be fast:\n\n- Early bailouts for non-SQL code\n- Efficient SQL detection heuristics\n- Skips very long queries (\u003e10,000 characters)\n- Gracefully handles missing dependencies\n\n## Available Error Identifiers\n\n| Identifier | Rule | Description |\n|------------|------|-------------|\n| `pdoSql.sqlSyntax` | SQL Syntax Validation | SQL syntax error detected |\n| `pdoSql.missingParameter` | Parameter Bindings | Parameter expected in SQL but missing from `execute()` array |\n| `pdoSql.extraParameter` | Parameter Bindings | Parameter in `execute()` array but not used in SQL |\n| `pdoSql.missingBinding` | Parameter Bindings | Parameter expected but no `bindValue()`/`bindParam()` found |\n| `pdoSql.extraBinding` | Parameter Bindings | Parameter bound but not used in SQL |\n| `pdoSql.columnMismatch` | SELECT Column Validation | Column name typo detected (case-sensitive) |\n| `pdoSql.columnMissing` | SELECT Column Validation | PHPDoc property missing from SELECT  |\n| `pdoSql.fetchTypeMismatch` | SELECT Column Validation | Fetch method doesn't match PHPDoc type structure |\n| `pdoSql.missingFalseType` | SELECT Column Validation | Missing `\\|false` union type for `fetch()`/`fetchObject()` |\n| `pdoSql.selfReferenceCondition` | Self-Reference Detection | Self-referencing condition in JOIN or WHERE clause |\n| `pdoSql.invalidTableReference` | Invalid Table Reference Detection | Invalid table or alias name in qualified column reference |\n| `pdoSql.mySqlSpecific` | MySQL-Specific Syntax | MySQL-specific function with portable alternative |\n| `pdoSql.tautologicalCondition` | Tautological Condition Detection | Always-true or always-false condition detected |\n\n### Ignoring Specific Errors\n\nAll errors from this extension have custom identifiers that allow you to selectively ignore them in your `phpstan.neon`:\n\n```neon\nparameters:\n    ignoreErrors:\n        # Ignore all SQL syntax errors\n        - identifier: pdoSql.sqlSyntax\n\n        # Ignore all parameter binding errors\n        - identifier: pdoSql.missingParameter\n        - identifier: pdoSql.extraParameter\n        - identifier: pdoSql.missingBinding\n        - identifier: pdoSql.extraBinding\n\n        # Ignore all SELECT column validation errors\n        - identifier: pdoSql.columnMismatch\n        - identifier: pdoSql.columnMissing\n        - identifier: pdoSql.fetchTypeMismatch\n        - identifier: pdoSql.missingFalseType\n\n        # Ignore all self-reference detection errors\n        - identifier: pdoSql.selfReferenceCondition\n\n        # Ignore all invalid table reference detection errors\n        - identifier: pdoSql.invalidTableReference\n\n        # Ignore all MySQL-specific syntax errors\n        - identifier: pdoSql.mySqlSpecific\n\n        # Ignore all tautological condition errors\n        - identifier: pdoSql.tautologicalCondition\n```\n\nYou can also ignore errors by path or message pattern:\n\n```neon\nparameters:\n    ignoreErrors:\n        # Ignore SQL syntax errors in migration files\n        -\n            identifier: pdoSql.sqlSyntax\n            path: */migrations/*\n\n        # Ignore missing parameter errors for a specific parameter\n        -\n            message: '#Missing parameter :legacy_id#'\n            identifier: pdoSql.missingParameter\n```\n\n## Playground\n\nWant 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.\n\n## Developer Tools\n\n### `ddt()` - Dump Debug Type\n\nThe `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.\n\n**Usage in PHPUnit tests:**\n\n```php\nuse PHPUnit\\Framework\\TestCase;\n\nclass MyTest extends TestCase\n{\n    public function testExample(): void\n    {\n        $row = $stmt-\u003efetch(); // Fetch data from database\n        ddt($row); // Dumps type and stops execution\n    }\n}\n```\n\n**Terminal output:**\n\n```php\n/**\n * @phpstan-type Item object{\n *  id: int,\n *  name: string,\n *  status: int,\n * }\n */\n```\n\nSimply copy the output and paste it into your code as a type annotation!\n\n**Supported types:**\n\n- **Objects** (stdClass and class instances): Shows public properties as `object{...}` shape\n- **Associative arrays**: Formatted as `array{key: type, ...}`\n- **Sequential arrays**: Formatted as `array\u003cint, type\u003e`\n- **Nested structures**: Handles nesting up to 5 levels deep\n- **All scalar types**: int, float, string, bool, null\n\n**Type mapping:**\n\n| PHP Runtime Type | PHPStan Output |\n|-----------------|----------------|\n| `integer` | `int` |\n| `double` | `float` |\n| `string` | `string` |\n| `boolean` | `bool` |\n| `NULL` | `null` |\n| `array` (associative) | `array{key: type, ...}` |\n| `array` (sequential) | `array\u003cint, type\u003e` |\n| `object` | `object{prop: type, ...}` |\n\n**Examples:**\n\n```php\n// Nested objects\n$workflow = new stdClass();\n$workflow-\u003eid = 1;\n$workflow-\u003emetadata = new stdClass();\n$workflow-\u003emetadata-\u003ecreated_at = '2024-01-01';\n\nddt($workflow);\n\n// Output:\n/**\n * @phpstan-type Item object{\n *  id: int,\n *  metadata: object{\n *    created_at: string,\n *  },\n * }\n */\n```\n\n```php\n// Associative array\n$config = ['database' =\u003e 'mysql', 'port' =\u003e 3306];\nddt($config);\n\n// Output:\n/**\n * @phpstan-type Item array{\n *  database: string,\n *  port: int,\n * }\n */\n```\n\n```php\n// Sequential array\n$ids = [1, 2, 3, 4, 5];\nddt($ids);\n\n// Output:\n/**\n * @phpstan-type Item array\u003cint, int\u003e\n */\n```\n\n**Note:** The function calls `exit(0)` after dumping (like `dd()`), so execution stops. This is intentional for use in debugging/testing workflows.\n\n### `ddc()` - Dump Debug Class\n\nThe `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()`.\n\n**Usage in PHPUnit tests:**\n\n```php\nuse PHPUnit\\Framework\\TestCase;\n\nclass MyTest extends TestCase\n{\n    public function testExample(): void\n    {\n        $row = $stmt-\u003efetchObject(); // Fetch data from database\n        ddc($row); // Dumps class definition and stops execution\n    }\n}\n```\n\n**Terminal output:**\n\n```php\nclass Item\n{\n    public int $id;\n    public string $name;\n    public string $email;\n    public ?string $phone;\n}\n```\n\nSimply copy the output, rename the class, and use it as your view model!\n\n**Example workflow:**\n\n```php\n// 1. First, discover the structure using ddc()\n$stmt = $db-\u003equery(\"SELECT id, name, email, phone FROM users WHERE id = 1\");\n$row = $stmt-\u003efetchObject();\nddc($row);\n\n// 2. Create your view model class from the output\nclass UserViewModel\n{\n    public int $id;\n    public string $name;\n    public string $email;\n    public ?string $phone;\n}\n\n// 3. Use it with PDO::fetchObject()\n$stmt = $db-\u003equery(\"SELECT id, name, email, phone FROM users WHERE id = 1\");\n$user = $stmt-\u003efetchObject(UserViewModel::class);\n```\n\n**Supported types:**\n\n| PHP Runtime Value | Generated Type |\n|------------------|----------------|\n| `integer` | `int` |\n| `double` | `float` |\n| `string` | `string` |\n| `boolean` | `bool` |\n| `NULL` | `mixed` |\n| `array` | `array` |\n| `object` | `object` |\n\n**Note:** Like `ddt()`, this function calls `exit(0)` after dumping.\n\n## Development\n\nTo contribute to this project:\n\n1. Clone the repository:\n```bash\ngit clone https://github.com/pierresh/phpstan-pdo-mysql.git\ncd phpstan-pdo-mysql\n```\n\n2. Install dependencies:\n```bash\ncomposer install\n```\n\n3. Run tests:\n```bash\ncomposer test\n```\n\nThis will start PHPUnit watcher that automatically runs tests when files change.\n\nTo run tests once without watching:\n```bash\n./vendor/bin/phpunit\n```\n\n4. Analyze source code with PHPStan:\n```bash\ncomposer analyze\n```\n\nThis analyzes only the `./src` directory (excludes playground and test fixtures) at maximum level.\n\n5. Refactor code with Rector:\n```bash\ncomposer refactor:dry  # Preview changes without applying\ncomposer refactor      # Apply refactoring changes\n```\n\nRector is configured to modernize code to PHP 8.1+ standards with code quality improvements.\n\n6. Format code with Mago:\n```bash\ncomposer format:check  # Check formatting without making changes\ncomposer format        # Apply code formatting\n```\n\nMago provides consistent, opinionated code formatting for PHP 8.1+.\n\n7. Lint code with Mago:\n```bash\ncomposer lint          # Run Mago linter\n```\n\n8. Analyze code with Mago:\n```bash\ncomposer mago:analyze  # Run Mago static analyzer\n```\n\nMago's analyzer provides fast, type-level analysis to find logical errors and type mismatches.\n\n## License\n\nMIT\n\n## Contributing\n\nContributions welcome! Please open an issue or submit a pull request.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpierresh%2Fphpstan-pdo-mysql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpierresh%2Fphpstan-pdo-mysql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpierresh%2Fphpstan-pdo-mysql/lists"}