Skip to content

butschster/dbml-parser

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

42 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

DBML Parser for PHP

Support me on Patreon Latest Stable Version Build Status Total Downloads License

DBML-parser

A production-ready PHP 8.3+ parser for DBML (Database Markup Language), transforming human-readable database schemas into structured PHP objects. Generate migrations, ORM entities, documentation, or visualization tools from a single DBML source.

Table of Contents

Why DBML Parser?

DBML (Database Markup Language) is a simple, readable DSL for defining database structures. This parser enables you to:

  • Version Control Database Schemas: Store your database design in git-friendly text format
  • Generate Code Automatically: Create ORM entities, migrations, models, and documentation from DBML
  • Visualize Database Design: Build interactive schema diagrams and documentation sites
  • Share Database Specs: Communicate database structure with teams using human-readable syntax
  • Build Schema Tools: Create custom tools for schema validation, transformation, or analysis

This library was inspired by dbdiagram.io and built using the powerful phplrt parser toolkit.

Installation

Requirements:

  • PHP 8.3 or higher
  • Composer

Install via Composer:

composer require butschster/dbml-parser

Quick Start

Parse a DBML schema and access its components:

use Butschster\Dbml\DbmlParserFactory; // Create parser instance $parser = DbmlParserFactory::create(); // Parse DBML string $schema = $parser->parse(<<<DBML  Project ecommerce {  database_type: 'PostgreSQL'  Note: 'E-commerce database schema'  }    Table users {  id int [pk, increment]  email varchar(255) [unique, not null]  created_at timestamp [default: `now()`]  }    Table orders {  id int [pk, increment]  user_id int [not null, ref: > users.id]  status order_status  total decimal(10,2)  }    Enum order_status {  pending  processing  shipped  delivered  } DBML); // Access schema components foreach ($schema->getTables() as $table) { echo "Table: {$table->getName()}\n"; foreach ($table->getColumns() as $column) { echo " - {$column->getName()}: {$column->getType()->getName()}\n"; } }

Core Concepts

The parser transforms DBML into an Abstract Syntax Tree (AST) with these key node types:

Node Type Purpose Example
SchemaNode Root container for entire schema Top-level access to all components
ProjectNode Project metadata and settings Database type, version, notes
TableNode Table definition with columns Table users { ... }
ColumnNode Column with type and constraints id int [pk, not null]
IndexNode Single or composite index Indexes { (col1, col2) [unique] }
EnumNode Enum type definition Enum status { active, inactive }
RefNode Foreign key relationship Ref: orders.user_id > users.id
TableGroupNode Logical grouping of tables TableGroup core { users, roles }

Complete API Reference

Schema Operations

The SchemaNode is your entry point for accessing all parsed components.

Get All Tables

/** @var \Butschster\Dbml\Ast\SchemaNode $schema */ // Get all tables as array $tables = $schema->getTables(); // Returns: TableNode[] foreach ($tables as $table) { echo $table->getName() . "\n"; }

Get Specific Table

// Check if table exists if ($schema->hasTable('users')) { $table = $schema->getTable('users'); // Returns: TableNode } // Throws TableNotFoundException if not found try { $table = $schema->getTable('nonexistent'); } catch (\Butschster\Dbml\Exceptions\TableNotFoundException $e) { // Handle missing table }

Access Project Metadata

// Check if project is defined if ($schema->hasProject()) { $project = $schema->getProject(); // Returns: ProjectNode|null }

Get Table Groups

// Get all table groups $groups = $schema->getTableGroups(); // Returns: TableGroupNode[] // Check specific group if ($schema->hasTableGroup('core_tables')) { $group = $schema->getTableGroup('core_tables'); // Returns: TableGroupNode } // Throws TableGroupNotFoundException if not found

Get Enums

// Get all enums $enums = $schema->getEnums(); // Returns: EnumNode[] // Access specific enum if ($schema->hasEnum('user_status')) { $enum = $schema->getEnum('user_status'); // Returns: EnumNode } // Throws EnumNotFoundException if not found

Get Relationships

// Get all foreign key relationships $refs = $schema->getRefs(); // Returns: RefNode[] foreach ($refs as $ref) { $leftTable = $ref->getLeftTable()->getTable(); $rightTable = $ref->getRightTable()->getTable(); echo "{$leftTable} references {$rightTable}\n"; }

Complete Schema Example:

use Butschster\Dbml\Ast\SchemaNode; /** @var SchemaNode $schema */ // Project information $project = $schema->getProject(); $dbType = $project?->getSetting('database_type')->getValue(); // All components $allTables = $schema->getTables(); // All table definitions $allEnums = $schema->getEnums(); // All enum types $allRefs = $schema->getRefs(); // All relationships $allGroups = $schema->getTableGroups(); // All table groups // Component counts $tableCount = count($allTables); $enumCount = count($allEnums); $refCount = count($allRefs); echo "Database: {$dbType}, Tables: {$tableCount}, Enums: {$enumCount}\n";

Project Definition

The ProjectNode stores database-level metadata and configuration.

DBML Syntax:

Project my_app { database_type: 'PostgreSQL' note: 'Application database schema' } 

Access Project Properties

/** @var \Butschster\Dbml\Ast\ProjectNode $project */ $project = $schema->getProject(); // Get project name $name = $project->getName(); // Returns: string (e.g., 'my_app') // Get project note $note = $project->getNote(); // Returns: string|null // Get all settings $settings = $project->getSettings(); // Returns: SettingNode[]

Access Project Settings

// Check if setting exists if ($project->hasSetting('database_type')) { $setting = $project->getSetting('database_type'); // Returns: SettingNode $key = $setting->getKey(); // 'database_type' $value = $setting->getValue(); // 'PostgreSQL' } // Throws ProjectSettingNotFoundException if not found try { $setting = $project->getSetting('unknown_setting'); } catch (\Butschster\Dbml\Exceptions\ProjectSettingNotFoundException $e) { // Handle missing setting }

Get Node Position

// Get offset in source DBML for debugging $offset = $project->getOffset(); // Returns: int (character position in parsed string)

Complete Project Example:

use Butschster\Dbml\Ast\ProjectNode; /** @var ProjectNode $project */ echo "Project: {$project->getName()}\n"; echo "Note: {$project->getNote()}\n\n"; echo "Settings:\n"; foreach ($project->getSettings() as $setting) { echo " {$setting->getKey()}: {$setting->getValue()}\n"; } // Common settings to check $dbType = $project->hasSetting('database_type') ? $project->getSetting('database_type')->getValue() : 'unknown'; $version = $project->hasSetting('version') ? $project->getSetting('version')->getValue() : null;

Table Operations

The TableNode represents a database table with columns, indexes, and relationships.

DBML Syntax:

Table users as U { id int [pk, increment] email varchar(255) [unique, not null] created_at timestamp Note: 'User accounts' Indexes { email [unique] (email, created_at) [name: 'email_created_idx'] } } 

Access Table Properties

/** @var \Butschster\Dbml\Ast\TableNode $table */ $table = $schema->getTable('users'); // Table name $name = $table->getName(); // Returns: string (e.g., 'users') // Table alias (from "as U") $alias = $table->getAlias(); // Returns: string|null (e.g., 'U') // Table note $note = $table->getNote(); // Returns: string|null // Source position $offset = $table->getOffset(); // Returns: int

Get Table Columns

// Get all columns $columns = $table->getColumns(); // Returns: ColumnNode[] (associative array keyed by column name) foreach ($columns as $columnName => $column) { echo "{$columnName}: {$column->getType()->getName()}\n"; } // Check if column exists if ($table->hasColumn('email')) { $column = $table->getColumn('email'); // Returns: ColumnNode } // Throws ColumnNotFoundException if not found try { $column = $table->getColumn('nonexistent'); } catch (\Butschster\Dbml\Exceptions\ColumnNotFoundException $e) { // Handle missing column }

Get Table Indexes

// Get all indexes $indexes = $table->getIndexes(); // Returns: IndexNode[] foreach ($indexes as $index) { $columns = $index->getColumns(); $indexName = $index->getName(); $isPrimary = $index->isPrimaryKey(); $isUnique = $index->isUnique(); }

Complete Table Example:

use Butschster\Dbml\Ast\TableNode; /** @var TableNode $table */ echo "Table: {$table->getName()}"; if ($alias = $table->getAlias()) { echo " (alias: {$alias})"; } echo "\n"; if ($note = $table->getNote()) { echo "Note: {$note}\n"; } echo "\nColumns:\n"; foreach ($table->getColumns() as $column) { $type = $column->getType()->getName(); $size = $column->getType()->getSize(); $nullable = $column->isNull() ? 'NULL' : 'NOT NULL'; $pk = $column->isPrimaryKey() ? '[PK]' : ''; echo " {$column->getName()} {$type}"; if ($size) {echo "({$size})";} echo " {$nullable} {$pk}\n"; } echo "\nIndexes:\n"; foreach ($table->getIndexes() as $index) { $indexType = $index->isPrimaryKey() ? 'PRIMARY' : ($index->isUnique() ? 'UNIQUE' : 'INDEX'); $cols = implode(', ', array_map(fn($c) => $c->getValue(), $index->getColumns())); echo " {$indexType} ({$cols})"; if ($name = $index->getName()) {echo " [{$name}]";} echo "\n"; }

Column Properties

The ColumnNode represents a table column with its type, constraints, and metadata.

DBML Syntax:

Table products { id int [pk, increment] name varchar(255) [not null] price decimal(10,2) [default: 0.00] status product_status [not null] created_at timestamp [default: `now()`] note: 'Product catalog' } 

Access Basic Properties

/** @var \Butschster\Dbml\Ast\Table\ColumnNode $column */ $column = $table->getColumn('price'); // Column name $name = $column->getName(); // Returns: string (e.g., 'price') // Source position $offset = $column->getOffset(); // Returns: int // Column note $note = $column->getNote(); // Returns: string|null

Get Column Type

// Get type information $type = $column->getType(); // Returns: TypeNode $typeName = $type->getName(); // Returns: string (e.g., 'decimal', 'varchar', 'int') $size = $type->getSize(); // Returns: int|null (first size parameter, e.g., 10 from decimal(10,2)) $sizeArray = $type->getSizeArray(); // Returns: int[] (all size parameters, e.g., [10, 2] from decimal(10,2)) $offset = $type->getOffset(); // Returns: int

Type Examples:

// varchar(255) $type->getName(); // 'varchar' $type->getSize(); // 255 $type->getSizeArray(); // [255] // decimal(10,2) $type->getName(); // 'decimal' $type->getSize(); // 10 $type->getSizeArray(); // [10, 2] // int (no size) $type->getName(); // 'int' $type->getSize(); // null $type->getSizeArray(); // []

Check Column Constraints

// Primary key $isPrimaryKey = $column->isPrimaryKey(); // Returns: bool // Auto-increment $isIncrement = $column->isIncrement(); // Returns: bool // Unique constraint $isUnique = $column->isUnique(); // Returns: bool // Nullable $isNullable = $column->isNull(); // Returns: bool (true = NULL, false = NOT NULL)

Get Default Value

// Get default value $default = $column->getDefault(); // Returns: AbstractValue|null if ($default !== null) { $value = $default->getValue(); // Type depends on value node: // - IntNode: int // - FloatNode: float // - StringNode: string|int|float|bool // - BooleanNode: bool // - NullNode: null // - ExpressionNode: string (e.g., 'now()') }

Default Value Types:

use Butschster\Dbml\Ast\Values\{IntNode, FloatNode, StringNode, BooleanNode, NullNode, ExpressionNode}; $default = $column->getDefault(); // Check value type if ($default instanceof IntNode) { $intValue = $default->getValue(); // int } if ($default instanceof FloatNode) { $floatValue = $default->getValue(); // float } if ($default instanceof StringNode) { $stringValue = $default->getValue(); // string|int|float|bool } if ($default instanceof BooleanNode) { $boolValue = $default->getValue(); // bool } if ($default instanceof NullNode) { $nullValue = $default->getValue(); // null } if ($default instanceof ExpressionNode) { $expression = $default->getValue(); // string (e.g., 'now()') }

Get Additional Settings

// Get custom settings (beyond standard constraints) $settings = $column->getSettings(); // Returns: SettingWithValueNode[] foreach ($settings as $setting) { $name = $setting->getName(); // string $value = $setting->getValue(); // AbstractValue }

Get Column Relationships

// Get foreign key references defined inline $refs = $column->getRefs(); // Returns: RefNode[] foreach ($refs as $ref) { $rightTable = $ref->getRightTable()->getTable(); $rightColumns = $ref->getRightTable()->getColumns(); }

Complete Column Example:

use Butschster\Dbml\Ast\Table\ColumnNode; /** @var ColumnNode $column */ // Basic info echo "Column: {$column->getName()}\n"; echo "Type: {$column->getType()->getName()}"; if ($size = $column->getType()->getSize()) { echo "({$size})"; } echo "\n"; // Constraints $constraints = []; if ($column->isPrimaryKey()) {$constraints[] = 'PRIMARY KEY';} if ($column->isIncrement()) {$constraints[] = 'AUTO_INCREMENT';} if ($column->isUnique()) {$constraints[] = 'UNIQUE';} if (!$column->isNull()) {$constraints[] = 'NOT NULL';} if (!empty($constraints)) { echo "Constraints: " . implode(', ', $constraints) . "\n"; } // Default value if ($default = $column->getDefault()) { $defaultValue = $default->getValue(); echo "Default: "; if ($default instanceof \Butschster\Dbml\Ast\Values\ExpressionNode) { echo "`{$defaultValue}`"; } else { echo var_export($defaultValue, true); } echo "\n"; } // Note if ($note = $column->getNote()) { echo "Note: {$note}\n"; } // Relationships if ($refs = $column->getRefs()) { echo "References:\n"; foreach ($refs as $ref) { $table = $ref->getRightTable()->getTable(); $cols = implode(', ', $ref->getRightTable()->getColumns()); echo " -> {$table}({$cols})\n"; } }

Index Configuration

The IndexNode represents a table index, which can be single-column or composite.

DBML Syntax:

Table products { id int merchant_id int status varchar Indexes { id [pk] (merchant_id, status) [name: 'merchant_status_idx', type: hash] email [unique] } } 

Access Index Properties

/** @var \Butschster\Dbml\Ast\Table\IndexNode $index */ $index = $table->getIndexes()[0]; // Index name (optional) $name = $index->getName(); // Returns: string|null (e.g., 'merchant_status_idx') // Index type (optional) $type = $index->getType(); // Returns: string|null (e.g., 'hash', 'btree') // Index note $note = $index->getNote(); // Returns: string|null // Check if primary key $isPrimary = $index->isPrimaryKey(); // Returns: bool // Check if unique $isUnique = $index->isUnique(); // Returns: bool

Get Indexed Columns

// Get columns in the index $columns = $index->getColumns(); // Returns: AbstractValue[] (StringNode or ExpressionNode) foreach ($columns as $column) { $columnName = $column->getValue(); // For StringNode: column name // For ExpressionNode: expression like 'LOWER(email)' } // Example: Single column index // Indexes { email [unique] } $columns = $index->getColumns(); // Returns: [StringNode('email')] // Example: Composite index // Indexes { (merchant_id, status) [name: 'idx'] } $columns = $index->getColumns(); // Returns: [StringNode('merchant_id'), StringNode('status')] // Example: Expression index // Indexes { `LOWER(email)` [unique] } $columns = $index->getColumns(); // Returns: [ExpressionNode('LOWER(email)')]

Get Custom Settings

// Get all settings (including custom ones) $settings = $index->getSettings(); // Returns: array (mixed setting types) // Settings can include: // - PrimaryKeyNode // - UniqueNode // - NoteNode // - SettingWithValueNode (for name, type, and custom settings)

Complete Index Example:

use Butschster\Dbml\Ast\Table\IndexNode; use Butschster\Dbml\Ast\Values\{StringNode, ExpressionNode}; /** @var IndexNode $index */ // Index type if ($index->isPrimaryKey()) { echo "PRIMARY KEY"; } elseif ($index->isUnique()) { echo "UNIQUE INDEX"; } else { echo "INDEX"; } // Index name if ($name = $index->getName()) { echo " {$name}"; } // Columns $columnNames = array_map( fn($col) => $col->getValue(), $index->getColumns() ); echo " (" . implode(', ', $columnNames) . ")"; // Index type (hash, btree, etc.) if ($type = $index->getType()) { echo " USING {$type}"; } // Note if ($note = $index->getNote()) { echo "\n Note: {$note}"; } echo "\n"; // Determine if composite $isComposite = count($index->getColumns()) > 1; echo $isComposite ? " (Composite index)\n" : " (Single column)\n"; // Check for expression columns $hasExpressions = array_reduce( $index->getColumns(), fn($has, $col) => $has || $col instanceof ExpressionNode, false ); if ($hasExpressions) { echo " (Contains expressions)\n"; }

Enum Management

The EnumNode represents an enumeration type that can be used as a column type.

DBML Syntax:

Enum order_status { pending processing shipped delivered [note: 'Order has been delivered to customer'] } 

Access Enum Properties

/** @var \Butschster\Dbml\Ast\EnumNode $enum */ $enum = $schema->getEnum('order_status'); // Enum name $name = $enum->getName(); // Returns: string (e.g., 'order_status') // Number of values $count = $enum->count(); // Returns: int (e.g., 4) // Source position $offset = $enum->getOffset(); // Returns: int

Get Enum Values

// Get all values $values = $enum->getValues(); // Returns: ValueNode[] (associative array keyed by value name) foreach ($values as $valueName => $valueNode) { echo "{$valueName}: {$valueNode->getValue()}\n"; if ($note = $valueNode->getNote()) { echo " Note: {$note}\n"; } } // Check if value exists if ($enum->hasValue('shipped')) { $value = $enum->getValue('shipped'); // Returns: ValueNode } // Throws EnumValueNotFoundException if not found try { $value = $enum->getValue('nonexistent'); } catch (\Butschster\Dbml\Exceptions\EnumValueNotFoundException $e) { // Handle missing enum value }

Access Value Properties

/** @var \Butschster\Dbml\Ast\Enum\ValueNode $value */ $value = $enum->getValue('delivered'); // Value name $name = $value->getValue(); // Returns: string (e.g., 'delivered') // Value note $note = $value->getNote(); // Returns: string|null // Source position $offset = $value->getOffset(); // Returns: int

Complete Enum Example:

use Butschster\Dbml\Ast\EnumNode; use Butschster\Dbml\Ast\Enum\ValueNode; /** @var EnumNode $enum */ echo "Enum: {$enum->getName()}\n"; echo "Values: {$enum->count()}\n\n"; // List all values foreach ($enum->getValues() as $valueName => $value) { echo " - {$valueName}"; if ($note = $value->getNote()) { echo " // {$note}"; } echo "\n"; } // Check if specific values exist $requiredValues = ['pending', 'processing', 'completed']; foreach ($requiredValues as $required) { if (!$enum->hasValue($required)) { echo "Warning: Missing required value '{$required}'\n"; } } // Generate PHP enum class echo "\nenum {$enum->getName()}: string {\n"; foreach ($enum->getValues() as $valueName => $value) { echo " case " . strtoupper($valueName) . " = '{$valueName}';\n"; } echo "}\n";

Table Groups

The TableGroupNode represents a logical grouping of related tables.

DBML Syntax:

TableGroup core_tables { users roles permissions } TableGroup e_commerce { products orders order_items } 

Access Group Properties

/** @var \Butschster\Dbml\Ast\TableGroupNode $group */ $group = $schema->getTableGroup('core_tables'); // Group name $name = $group->getName(); // Returns: string (e.g., 'core_tables') // Source position $offset = $group->getOffset(); // Returns: int

Get Group Tables

// Get all table names in group $tables = $group->getTables(); // Returns: string[] (array of table names) foreach ($tables as $tableName) { if ($schema->hasTable($tableName)) { $table = $schema->getTable($tableName); // Process table } } // Check if specific table is in group if ($group->hasTable('users')) { echo "users table is in {$group->getName()} group\n"; }

Complete Table Group Example:

use Butschster\Dbml\Ast\TableGroupNode; use Butschster\Dbml\Ast\SchemaNode; /** @var TableGroupNode $group */ /** @var SchemaNode $schema */ echo "Table Group: {$group->getName()}\n"; echo "Tables (" . count($group->getTables()) . "):\n"; foreach ($group->getTables() as $tableName) { echo " - {$tableName}"; if ($schema->hasTable($tableName)) { $table = $schema->getTable($tableName); $columnCount = count($table->getColumns()); echo " ({$columnCount} columns)"; } else { echo " [TABLE NOT FOUND]"; } echo "\n"; } // Group tables by their groups $allGroups = $schema->getTableGroups(); $groupedTables = []; foreach ($allGroups as $grp) { foreach ($grp->getTables() as $tableName) { $groupedTables[$tableName][] = $grp->getName(); } } // Find tables in multiple groups foreach ($groupedTables as $tableName => $groups) { if (count($groups) > 1) { echo "Table '{$tableName}' is in multiple groups: " . implode(', ', $groups) . "\n"; } }

Relationships (Refs)

The RefNode represents a foreign key relationship between tables.

DBML Syntax:

// Inline relationship Table orders { user_id int [ref: > users.id] } // Standalone relationship (short form) Ref: orders.user_id > users.id // Standalone relationship (long form with actions) Ref order_user_fk: products.merchant_id > merchants.id [ delete: cascade, update: no action ] // Composite foreign key Ref: order_items.(order_id, product_id) > orders.(id, product_id) // Relationship types: // > : many-to-one // < : one-to-many // - : one-to-one // Grouped relationships Ref { products.category_id > categories.id products.merchant_id > merchants.id } 

Access Relationship Properties

/** @var \Butschster\Dbml\Ast\RefNode $ref */ $ref = $schema->getRefs()[0]; // Relationship name (optional) $name = $ref->getName(); // Returns: string|null (e.g., 'order_user_fk') // Source position $offset = $ref->getOffset(); // Returns: int

Get Relationship Type

// Get relationship type $type = $ref->getType(); // Returns: TypeNode (one of the following) use Butschster\Dbml\Ast\Ref\Type\{ManyToOneNode, OneToManyNode, OneToOneNode}; if ($type instanceof ManyToOneNode) { echo "Many-to-One (>)\n"; } elseif ($type instanceof OneToManyNode) { echo "One-to-Many (<)\n"; } elseif ($type instanceof OneToOneNode) { echo "One-to-One (-)\n"; }

Get Referenced Tables

// Left side of relationship (source) $leftTable = $ref->getLeftTable(); // Returns: LeftTableNode $leftTableName = $leftTable->getTable(); // Returns: string (e.g., 'orders') $leftColumns = $leftTable->getColumns(); // Returns: string[] (e.g., ['user_id']) // Right side of relationship (target) $rightTable = $ref->getRightTable(); // Returns: RightTableNode $rightTableName = $rightTable->getTable(); // Returns: string (e.g., 'users') $rightColumns = $rightTable->getColumns(); // Returns: string[] (e.g., ['id'])

Composite Key Example:

Ref: order_items.(order_id, product_id) > orders.(id, product_id) 
$leftColumns = $ref->getLeftTable()->getColumns(); // Returns: ['order_id', 'product_id'] $rightColumns = $ref->getRightTable()->getColumns(); // Returns: ['id', 'product_id']

Get Referential Actions

// Get all actions $actions = $ref->getActions(); // Returns: ActionNode[] (associative array keyed by action name) foreach ($actions as $actionName => $action) { echo "{$actionName}: {$action->getAction()}\n"; } // Check if action exists if ($ref->hasAction('delete')) { $action = $ref->getAction('delete'); // Returns: OnDeleteNode $actionType = $action->getAction(); // Returns: string (cascade, restrict, set null, set default, no action) } // Throws RefActionNotFoundException if not found try { $action = $ref->getAction('nonexistent'); } catch (\Butschster\Dbml\Exceptions\RefActionNotFoundException $e) { // Handle missing action }

Action Types:

Action Description
cascade Automatically update/delete related rows
restrict Prevent action if related rows exist
set null Set foreign key to NULL
set default Set foreign key to default value
no action No automatic action (similar to restrict)

Action Node Types:

use Butschster\Dbml\Ast\Ref\Action\{OnDeleteNode, OnUpdateNode}; // Check action type if ($action instanceof OnDeleteNode) { echo "ON DELETE {$action->getAction()}\n"; } if ($action instanceof OnUpdateNode) { echo "ON UPDATE {$action->getAction()}\n"; } // Both extend ActionNode $actionName = $action->getName(); // 'delete' or 'update' $actionType = $action->getAction(); // 'cascade', 'restrict', etc.

Complete Relationship Example:

use Butschster\Dbml\Ast\RefNode; use Butschster\Dbml\Ast\Ref\Type\{ManyToOneNode, OneToManyNode, OneToOneNode}; /** @var RefNode $ref */ // Relationship name if ($name = $ref->getName()) { echo "Relationship: {$name}\n"; } // Tables and columns $left = $ref->getLeftTable(); $right = $ref->getRightTable(); echo "{$left->getTable()}." . implode(', ', $left->getColumns()); // Relationship type $type = $ref->getType(); if ($type instanceof ManyToOneNode) { echo " > "; } elseif ($type instanceof OneToManyNode) { echo " < "; } else { echo " - "; } echo "{$right->getTable()}." . implode(', ', $right->getColumns()); echo "\n"; // Actions if ($actions = $ref->getActions()) { echo "Actions:\n"; foreach ($actions as $actionName => $action) { echo " ON " . strtoupper($actionName) . " {$action->getAction()}\n"; } } // Generate SQL $leftCols = implode(', ', $left->getColumns()); $rightCols = implode(', ', $right->getColumns()); echo "\nSQL:\n"; echo "ALTER TABLE {$left->getTable()}\n"; echo " ADD CONSTRAINT " . ($name ?: "fk_{$left->getTable()}_{$right->getTable()}") . "\n"; echo " FOREIGN KEY ({$leftCols})\n"; echo " REFERENCES {$right->getTable()}({$rightCols})"; if ($ref->hasAction('delete')) { echo "\n ON DELETE " . strtoupper($ref->getAction('delete')->getAction()); } if ($ref->hasAction('update')) { echo "\n ON UPDATE " . strtoupper($ref->getAction('update')->getAction()); } echo ";\n";

Advanced Usage

Validating Schema Integrity

use Butschster\Dbml\Ast\SchemaNode; function validateSchema(SchemaNode $schema): array { $errors = []; // Check for orphaned foreign keys foreach ($schema->getRefs() as $ref) { $leftTable = $ref->getLeftTable()->getTable(); $rightTable = $ref->getRightTable()->getTable(); if (!$schema->hasTable($leftTable)) { $errors[] = "Reference from non-existent table: {$leftTable}"; } if (!$schema->hasTable($rightTable)) { $errors[] = "Reference to non-existent table: {$rightTable}"; } // Validate columns exist if ($schema->hasTable($leftTable)) { $table = $schema->getTable($leftTable); foreach ($ref->getLeftTable()->getColumns() as $col) { if (!$table->hasColumn($col)) { $errors[] = "Column {$leftTable}.{$col} not found"; } } } } // Check for duplicate column names foreach ($schema->getTables() as $table) { $columnNames = array_map(fn($c) => $c->getName(), $table->getColumns()); $duplicates = array_filter( array_count_values($columnNames), fn($count) => $count > 1 ); foreach ($duplicates as $colName => $count) { $errors[] = "Duplicate column '{$colName}' in table {$table->getName()}"; } } return $errors; } $errors = validateSchema($schema); if (!empty($errors)) { foreach ($errors as $error) { echo "Error: {$error}\n"; } }

Generating Documentation

use Butschster\Dbml\Ast\SchemaNode; function generateMarkdownDocs(SchemaNode $schema): string { $md = "# Database Schema\n\n"; if ($project = $schema->getProject()) { $md .= "**Project:** {$project->getName()}\n\n"; if ($note = $project->getNote()) { $md .= "> {$note}\n\n"; } } // Tables $md .= "## Tables\n\n"; foreach ($schema->getTables() as $table) { $md .= "### {$table->getName()}\n\n"; if ($note = $table->getNote()) { $md .= "*{$note}*\n\n"; } // Columns table $md .= "| Column | Type | Constraints |\n"; $md .= "|--------|------|-------------|\n"; foreach ($table->getColumns() as $column) { $type = $column->getType()->getName(); if ($size = $column->getType()->getSize()) { $type .= "({$size})"; } $constraints = []; if ($column->isPrimaryKey()) {$constraints[] = 'PK';} if ($column->isUnique()) {$constraints[] = 'UNIQUE';} if (!$column->isNull()) {$constraints[] = 'NOT NULL';} if ($column->isIncrement()) {$constraints[] = 'AUTO_INCREMENT';} $md .= "| {$column->getName()} | {$type} | " . implode(', ', $constraints) . " |\n"; } $md .= "\n"; } // Enums if (!empty($schema->getEnums())) { $md .= "## Enums\n\n"; foreach ($schema->getEnums() as $enum) { $md .= "### {$enum->getName()}\n\n"; foreach ($enum->getValues() as $value) { $md .= "- `{$value->getValue()}`"; if ($note = $value->getNote()) { $md .= " - {$note}"; } $md .= "\n"; } $md .= "\n"; } } return $md; } $markdown = generateMarkdownDocs($schema); file_put_contents('schema.md', $markdown);

Converting to Different Formats

use Butschster\Dbml\Ast\SchemaNode; function convertToArray(SchemaNode $schema): array { $result = []; // Project if ($project = $schema->getProject()) { $result['project'] = [ 'name' => $project->getName(), 'note' => $project->getNote(), 'settings' => array_map( fn($s) => ['key' => $s->getKey(), 'value' => $s->getValue()], $project->getSettings() ), ]; } // Tables $result['tables'] = []; foreach ($schema->getTables() as $table) { $result['tables'][$table->getName()] = [ 'alias' => $table->getAlias(), 'note' => $table->getNote(), 'columns' => array_map(function($col) { return [ 'name' => $col->getName(), 'type' => $col->getType()->getName(), 'size' => $col->getType()->getSizeArray(), 'primary_key' => $col->isPrimaryKey(), 'unique' => $col->isUnique(), 'nullable' => $col->isNull(), 'increment' => $col->isIncrement(), 'default' => $col->getDefault()?->getValue(), 'note' => $col->getNote(), ]; }, $table->getColumns()), 'indexes' => array_map(function($idx) { return [ 'name' => $idx->getName(), 'columns' => array_map(fn($c) => $c->getValue(), $idx->getColumns()), 'primary_key' => $idx->isPrimaryKey(), 'unique' => $idx->isUnique(), 'type' => $idx->getType(), ]; }, $table->getIndexes()), ]; } // Enums $result['enums'] = []; foreach ($schema->getEnums() as $enum) { $result['enums'][$enum->getName()] = array_map( fn($v) => ['value' => $v->getValue(), 'note' => $v->getNote()], $enum->getValues() ); } // Relationships $result['relationships'] = array_map(function($ref) { $type = match (get_class($ref->getType())) { \Butschster\Dbml\Ast\Ref\Type\ManyToOneNode::class => 'many-to-one', \Butschster\Dbml\Ast\Ref\Type\OneToManyNode::class => 'one-to-many', \Butschster\Dbml\Ast\Ref\Type\OneToOneNode::class => 'one-to-one', }; return [ 'name' => $ref->getName(), 'type' => $type, 'from' => [ 'table' => $ref->getLeftTable()->getTable(), 'columns' => $ref->getLeftTable()->getColumns(), ], 'to' => [ 'table' => $ref->getRightTable()->getTable(), 'columns' => $ref->getRightTable()->getColumns(), ], 'actions' => array_map( fn($a) => ['name' => $a->getName(), 'action' => $a->getAction()], $ref->getActions() ), ]; }, $schema->getRefs()); return $result; } // Convert to JSON $json = json_encode(convertToArray($schema), JSON_PRETTY_PRINT); file_put_contents('schema.json', $json); // Convert to YAML $yaml = yaml_emit(convertToArray($schema)); file_put_contents('schema.yaml', $yaml);

Generating Migration Code

use Butschster\Dbml\Ast\{SchemaNode, TableNode}; function generateLaravelMigration(TableNode $table): string { $className = 'Create' . str_replace('_', '', ucwords($table->getName(), '_')) . 'Table'; $tableName = $table->getName(); $code = "<?php\n\n"; $code .= "use Illuminate\Database\Migrations\Migration;\n"; $code .= "use Illuminate\Database\Schema\Blueprint;\n"; $code .= "use Illuminate\Support\Facades\Schema;\n\n"; $code .= "return new class extends Migration\n{\n"; $code .= " public function up(): void\n {\n"; $code .= " Schema::create('{$tableName}', function (Blueprint \$table) {\n"; foreach ($table->getColumns() as $column) { $type = $column->getType()->getName(); $name = $column->getName(); // Convert DBML types to Laravel types $laravelType = match($type) { 'varchar' => 'string', 'int' => 'integer', 'bool', 'boolean' => 'boolean', 'text' => 'text', 'datetime' => 'dateTime', 'timestamp' => 'timestamp', default => $type, }; $line = " \$table->{$laravelType}('{$name}'"; if ($size = $column->getType()->getSize()) { $line .= ", {$size}"; } $line .= ")"; if ($column->isUnique()) {$line .= "->unique()";} if (!$column->isNull()) {$line .= "->nullable(false)";} if ($column->isIncrement()) {$line .= "->autoIncrement()";} if ($default = $column->getDefault()) { $line .= "->default(" . var_export($default->getValue(), true) . ")"; } $line .= ";\n"; $code .= $line; } $code .= " });\n"; $code .= " }\n\n"; $code .= " public function down(): void\n {\n"; $code .= " Schema::dropIfExists('{$tableName}');\n"; $code .= " }\n"; $code .= "};\n"; return $code; } // Generate migrations for all tables foreach ($schema->getTables() as $table) { $migration = generateLaravelMigration($table); $filename = date('Y_m_d_His') . "_create_{$table->getName()}_table.php"; file_put_contents("database/migrations/{$filename}", $migration); }

Use Cases

1. Database Schema Version Control

Store your database design in DBML format alongside your application code:

// Store schema in version control $dbml = file_get_contents('schema/database.dbml'); $parser = DbmlParserFactory::create(); $schema = $parser->parse($dbml); // Validate before deployment $errors = validateSchema($schema); if (!empty($errors)) { throw new Exception("Schema validation failed: " . implode(', ', $errors)); }

2. ORM Entity Generation

Generate Doctrine, Eloquent, or Cycle ORM entities:

use Butschster\Dbml\Ast\TableNode; function generateCycleEntity(TableNode $table): string { $className = str_replace('_', '', ucwords($table->getName(), '_')); $code = "<?php\n\nnamespace App\Entities;\n\n"; $code .= "use Cycle\Annotated\Annotation as Cycle;\n\n"; $code .= "#[Cycle\Entity(table: '{$table->getName()}')]\n"; $code .= "class {$className}\n{\n"; foreach ($table->getColumns() as $column) { $code .= " #[Cycle\Column(type: '{$column->getType()->getName()}'"; if ($column->isPrimaryKey()) $code .= ", primary: true"; if (!$column->isNull()) $code .= ", nullable: false"; $code .= ")]\n"; $code .= " private {$column->getName()};\n\n"; } $code .= "}\n"; return $code; }

3. API Documentation Generation

Create OpenAPI/Swagger specifications from your database schema:

function generateOpenAPISchema(SchemaNode $schema): array { $openapi = [ 'openapi' => '3.0.0', 'info' => [ 'title' => $schema->getProject()?->getName() ?? 'API', 'version' => '1.0.0', ], 'components' => [ 'schemas' => [], ], ]; foreach ($schema->getTables() as $table) { $properties = []; $required = []; foreach ($table->getColumns() as $column) { $type = match($column->getType()->getName()) { 'int', 'integer' => 'integer', 'varchar', 'text' => 'string', 'bool', 'boolean' => 'boolean', 'decimal', 'float' => 'number', default => 'string', }; $properties[$column->getName()] = ['type' => $type]; if (!$column->isNull()) { $required[] = $column->getName(); } } $openapi['components']['schemas'][$table->getName()] = [ 'type' => 'object', 'properties' => $properties, 'required' => $required, ]; } return $openapi; }

4. Database Comparison Tool

Compare two DBML schemas to detect changes:

function compareSchemas(SchemaNode $old, SchemaNode $new): array { $changes = []; // New tables foreach ($new->getTables() as $table) { if (!$old->hasTable($table->getName())) { $changes[] = "Added table: {$table->getName()}"; } } // Removed tables foreach ($old->getTables() as $table) { if (!$new->hasTable($table->getName())) { $changes[] = "Removed table: {$table->getName()}"; } } // Modified tables foreach ($new->getTables() as $newTable) { if ($old->hasTable($newTable->getName())) { $oldTable = $old->getTable($newTable->getName()); // Column changes foreach ($newTable->getColumns() as $column) { if (!$oldTable->hasColumn($column->getName())) { $changes[] = "Added column: {$newTable->getName()}.{$column->getName()}"; } } } } return $changes; }

5. Schema Visualization

Generate GraphViz DOT format for visual diagrams:

function generateDotGraph(SchemaNode $schema): string { $dot = "digraph schema {\n"; $dot .= " node [shape=record];\n\n"; // Tables foreach ($schema->getTables() as $table) { $label = "{" . $table->getName() . "|"; $columns = []; foreach ($table->getColumns() as $column) { $col = $column->getName(); if ($column->isPrimaryKey()) {$col .= " (PK)";} $columns[] = $col; } $label .= implode("\\n", $columns) . "}"; $dot .= " {$table->getName()} [label=\"{$label}\"];\n"; } $dot .= "\n"; // Relationships foreach ($schema->getRefs() as $ref) { $from = $ref->getLeftTable()->getTable(); $to = $ref->getRightTable()->getTable(); $dot .= " {$from} -> {$to};\n"; } $dot .= "}\n"; return $dot; } // Generate PNG: dot -Tpng schema.dot -o schema.png file_put_contents('schema.dot', generateDotGraph($schema));

Error Handling

The parser throws specific exceptions for different error conditions:

use Butschster\Dbml\Exceptions\{ GrammarFileNotFoundException, TableNotFoundException, ColumnNotFoundException, EnumNotFoundException, EnumValueNotFoundException, TableGroupNotFoundException, ProjectSettingNotFoundException, RefActionNotFoundException }; try { // Parse DBML $parser = DbmlParserFactory::create(); $schema = $parser->parse($dbml); // Access components $table = $schema->getTable('users'); $column = $table->getColumn('email'); $enum = $schema->getEnum('status'); $value = $enum->getValue('active'); } catch (GrammarFileNotFoundException $e) { // Grammar file missing (shouldn't happen in normal usage) error_log("Parser initialization failed: " . $e->getMessage()); } catch (TableNotFoundException $e) { // Table doesn't exist in schema error_log("Table not found: " . $e->getMessage()); } catch (ColumnNotFoundException $e) { // Column doesn't exist in table error_log("Column not found: " . $e->getMessage()); } catch (EnumNotFoundException $e) { // Enum type doesn't exist error_log("Enum not found: " . $e->getMessage()); } catch (EnumValueNotFoundException $e) { // Enum value doesn't exist error_log("Enum value not found: " . $e->getMessage()); } catch (TableGroupNotFoundException $e) { // Table group doesn't exist error_log("Table group not found: " . $e->getMessage()); } catch (ProjectSettingNotFoundException $e) { // Project setting doesn't exist error_log("Project setting not found: " . $e->getMessage()); } catch (RefActionNotFoundException $e) { // Referential action doesn't exist error_log("Ref action not found: " . $e->getMessage()); } catch (\Phplrt\Contracts\Exception\RuntimeExceptionInterface $e) { // Parser error (syntax error in DBML) error_log("DBML parsing failed: " . $e->getMessage()); }

Best Practices:

  1. Always check existence before accessing using has*() methods
  2. Catch specific exceptions rather than generic Exception
  3. Validate DBML syntax before parsing in production
  4. Handle parser errors gracefully with user-friendly messages

Contributing

Contributions are welcome! Please follow these guidelines:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Write tests for your changes
  4. Ensure all tests pass (composer test)
  5. Commit your changes (git commit -m 'Add amazing feature')
  6. Push to the branch (git push origin feature/amazing-feature)
  7. Open a Pull Request

Credits

License

This package is open-sourced software licensed under the MIT license.


Support this project:

For more information about DBML syntax and features, visit www.dbml.org.

About

DBML parser for PHP8. It's a PHP parser for DBML syntax.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages