Generate PHPStan (and optionally Psalm) shape types directly from your database schema. Point the generator at a MySQL or PostgreSQL database via PDO and it produces a small PHP class whose docblock contains @phpstan-type (and also @psalm-type) definitions for every table.
Works great to keep static-analysis types in sync with your DB without hand-writing shapes.
- PHP 8.2+
ext-pdo- For analysis: PHPStan 1.10+ (optional) and/or Psalm (optional)
composer require --dev rkr/phpstan-types-from-sql
If you want to autoload generated types under your own namespace (recommended), add an autoload-dev mapping in your project’s composer.json, e.g.:
- In
composer.json→autoload-dev.psr-4:{ "App\\\Types\\\": "generated/" } - Then run
composer dump-autoload
- Composer exposes
vendor/bin/phpstan-sql-typesafter install. - Generate types directly from the command line without writing any PHP glue.
Parameters
- Database connection
-d,--db-dsn <dsn>Database DSN, e.g.mysql:host=127.0.0.1;dbname=your_db;charset=utf8mb4orpgsql:host=127.0.0.1;port=5432;dbname=your_db-u,--db-user <user>Database user-p,--db-password <password>Database password-s,--db-schema <name>Schema name (Postgres required unlesscurrent_schema()fits; optional for MySQL)
- Analyzer generation
-a,--array-shapesUsearray{...}shapes (default)-o,--object-shapesUseobject{...}shapes (MySQL); Postgres currently generates arrays--psalmInclude Psalm@psalm-typedefinitions--phpstanInclude PHPStan@phpstan-typedefinitions- If neither
--psalmnor--phpstanis provided, both are generated -c,--class-name <name>Generated class name (example:DbTypes)-n,--namespace <name>Generated namespace of the class (example:DbTypes)
- Output
--output <path>Write to a file; if omitted, writes to stdout
Examples
MySQL → file (object shapes, both analyzers):
vendor/bin/phpstan-sql-types \
-d "mysql:host=127.0.0.1;dbname=your_db;charset=utf8mb4" \
-u your_user -p your_password \
-c DbTypes -n DbTypes \
--output generated/DbTypes.php
PostgreSQL → stdout (array shapes, both analyzers):
vendor/bin/phpstan-sql-types \
-d "pgsql:host=127.0.0.1;port=5432;dbname=your_db" -s public \
-u your_user -p your_password \
-c DbTypes -n DbTypes > generated/DbTypes.php
Notes
- Postgres currently emits array shapes;
--object-shapesis ignored there. - If only
--psalmis requested for Postgres, the tool mirrors the PHPStan types as Psalm types for convenience.
- This example mirrors
test-mysql.phpbut with placeholder credentials.
<?php
use Kir\PhpstanTypesFromSql\MySQLStaticAnalyzerTypeGenerator;
use PDO;
require __DIR__ . '/vendor/autoload.php';
$pdo = new PDO(
'mysql:host=127.0.0.1;dbname=your_db;charset=utf8mb4',
'your_user',
'your_password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$gen = new MySQLStaticAnalyzerTypeGenerator(pdo: $pdo);
$contents = $gen->generate(
namespace: 'App\\Types', // Namespace for the generated class
className: 'DbTypes', // Class name to hold the docblock
databaseName: 'your_db', // MySQL database name
asArray: false, // object{...} when false, array{...} when true
full: true // require all keys (true) or allow partial (false)
);
file_put_contents(__DIR__ . '/generated/DbTypes.php', $contents);Notes
- MySQL generator emits both
@phpstan-typeand@psalm-typeper table. asArray=falseyieldsobject{...}shapes;asArray=trueyieldsarray{...}shapes.full=truemakes all keys required;full=falsegenerates a partial variant (..._Partial).
- This example mirrors
test-postgres.phpbut with placeholder credentials.
<?php
use Kir\PhpstanTypesFromSql\PostgresStaticAnalyzerTypeGenerator;
use PDO;
require __DIR__ . '/vendor/autoload.php';
$pdo = new PDO('pgsql:host=127.0.0.1;port=5432;dbname=your_db', 'your_user', 'your_password', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
]);
// Optional: fix the current schema for lookups
$pdo->exec('SET search_path TO public');
$gen = new PostgresStaticAnalyzerTypeGenerator(pdo: $pdo);
$contents = $gen->generate(
namespace: 'App\\Types', // Namespace for the generated class
className: 'DbTypes', // Class name to hold the docblock
databaseName: 'your_db', // Database name (defaults to current_database())
schemaName: 'public', // Schema name (defaults to current_schema())
asArray: true, // Currently generates array shapes
full: true
);
file_put_contents(__DIR__ . '/generated/DbTypes.php', $contents);Notes
- PostgreSQL generator currently emits
@phpstan-typearray shapes for each table. - Types are inferred from
information_schema.columnsvia a built-in Postgres type mapping.
- You can include only specific tables by adding one or more filters. The callable receives the table name and must return
trueto include it.
$gen->addFilter(static fn (string $table) => str_starts_with($table, 'public_'));
$gen->addFilter(static fn (string $table) => !in_array($table, ['migrations', 'audit_log'], true));
-
The generator writes an empty class with a large docblock. The docblock contains shape type aliases per table.
-
Examples below are abbreviated from files under
generated/in this repository. -
MySQL (object shape, excerpt from
generated/ClassName.php):
/**
* @phpstan-type TSsoRegistry object{
* public_token: string,
* private_token: string,
* authorized: int,
* payload: string,
* success_url: string,
* failure_url: string|null,
* expires_at: string|null,
* created_at: string,
* updated_at: string
* }
*
* @psalm-type TSsoRegistry object{
* public_token: string,
* private_token: string,
* authorized: int,
* payload: string,
* success_url: string,
* failure_url: string|null,
* expires_at: string|null,
* created_at: string,
* updated_at: string
* }
*/
class ClassName {}- PostgreSQL (array shape, excerpt from
generated/<ClassName>.php):
/**
* @phpstan-type TTokens array{
* persistent: bool,
* id: int,
* user_id: int,
* updated_at: string,
* created_at: string,
* expires_at: string|null,
* name: string|null,
* action: string,
* token: string
* }
*/
class ClassName {}These aliases can be referenced in PHPDoc throughout your codebase, for example:
// ...
/** @var TTokens $row */
private readonly array $row;
// ...
/**
* @param TSsoRegistry $record
*/
public function someMethod(array $record): void {
// ...
}- Ensure the generated class is autoloadable (e.g., via
autoload-devmapping) so PHPStan can load the docblock definitions during analysis. - You don’t need to reference the class directly; it’s sufficient that the file is autoloaded when PHPStan runs.
- Regenerate after schema changes and commit the generated PHP file if you want reproducible analysis in CI.
- Unknown or uncommon SQL types will raise an exception; extend or adjust your schema/types if needed.
- MySQL enums and sets are translated into string literal unions.
- MIT