butschster/dbml-parser

DBML (database markup language) parser written on PHP8.

Installs: 15 052

Dependents: 2

Suggesters: 0

Security: 0

Stars: 64

Watchers: 3

Forks: 4

Open Issues: 3

pkg:composer/butschster/dbml-parser

1.0.0 2026-01-09 08:06 UTC

This package is auto-updated.

Last update: 2026-01-09 08:08:55 UTC


README

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.