Skip to main content

Schema Builder

Introduction

Schema is a facade over Blueprint for creating, altering and dropping MySQL tables from PHP.

No migration runner

The package ships a schema builder only. There is no migration runner, no version tracking, no up() / down() classes, and no CLI. Call Schema::create() yourself, typically from a plugin activation hook, and guard re-runs with your own version option.

use BitApps\WPDatabase\Schema;

register_activation_hook(__FILE__, function () {
Schema::withWpPrefix()->create('orders', function ($table) {
$table->id();
$table->string('reference');
$table->timestamps();
});
});

Creating tables

Schema::create('orders', function ($table) {
$table->id();
$table->bigint('user_id')->unsigned()
->foreign('users', 'id')
->onDelete()->cascade();
$table->varchar('status', 32)->defaultValue('pending');
$table->decimal('total')->nullable();
$table->timestamps();
});

Schema::create() emits CREATE TABLE IF NOT EXISTS. The callback receives a Blueprint; column methods return the same Blueprint, so modifiers chain directly:

$table->varchar('slug', 100)->nullable()->unique();

Table prefixes

The schema builder uses the table name as-is. No prefix is applied by default, so existing tables are never relocated:

// Bare name: table resolves as "orders"
Schema::create('orders', fn ($table) => $table->id());

// Literal prefix: "custom_orders"
Schema::withPrefix('custom_')->create('orders', fn ($table) => $table->id());

// Same prefix Models use (Connection::getPrefix()): "wp_myplugin_orders"
Schema::withWpPrefix()->create('orders', fn ($table) => $table->id());

Use Schema::withWpPrefix() whenever the table backs a Model, so both resolve to the same name.

Table and column names are interpolated

Unlike the query builder, Blueprint writes table and column names into DDL without identifier validation or quoting. Schema names must be developer-authored constants; never pass user input to Schema or Blueprint.

Column types

The first argument is always the column name; the optional second argument sets the length (or, for ENUM / SET, an array of allowed values).

String / binary

MethodSQL type
char($name, $length = null)CHAR
varchar($name, $length = null)VARCHAR
binary($name, $length = null)BINARY
varbinary($name, $length = null)VARBINARY
json($name)JSON

binary() / varbinary() are column types, not modifiers: $table->binary('data', 16) defines a BINARY(16) column.

Text / blob

MethodSQL type
tinytext($name)TINYTEXT
text($name)TEXT
mediumtext($name)MEDIUMTEXT
longtext($name)LONGTEXT
tinyblob($name)TINYBLOB
blob($name)BLOB
mediumblob($name)MEDIUMBLOB
longblob($name)LONGBLOB

Numeric

MethodSQL typeNotes
bit($name, $length = null)BIT
tinyint($name, $length = null)TINYINT
bool($name)BOOL
boolean($name)BOOLEANAlias of bool
smallint($name, $length = null)SMALLINT
mediumint($name, $length = null)MEDIUMINT
int($name, $length = null)INT
integer($name, $length = null)INTEGERAlias of int
bigint($name, $length = null)BIGINT
float($name, $length = null)FLOAT
double($name, $length = null)DOUBLE
double_precision($name, $length = null)DOUBLE PRECISIONUnderscore maps to a space
decimal($name, $length = null, $scale = null)DECIMALOnly decimal / dec accept a third argument
dec($name, $length = null, $scale = null)DECAlias of decimal

Enumerated

$table->enum('status', ['draft', 'published', 'archived']);
$table->set('permissions', ['read', 'write', 'delete']);

Date / time

MethodSQL type
date($name)DATE
datetime($name)DATETIME
timestamp($name)TIMESTAMP
time($name)TIME
year($name)YEAR

Column modifiers

Chain these immediately after a type call:

$table->varchar('email', 190)->nullable()->unique();
$table->tinyint('priority')->unsigned()->defaultValue(0)->index();
ModifierEffect
nullable()Emits NULL instead of NOT NULL.
defaultValue($value)Adds DEFAULT $value. Integers, CURRENT_TIMESTAMP and NULL are unquoted; other strings are single-quoted.
unsigned()Adds UNSIGNED.
zeroFill()Adds ZEROFILL.
primary()Registers the column in the PRIMARY KEY clause and forces NOT NULL.
unique($column = null)Registers a UNIQUE INDEX on the current column, or on the given column name / array of names.
index($type = null)Registers an INDEX on the current column.
length($length)Sets the column length; accepts an array for ENUM / SET values.
note

index($type) concatenates the argument directly onto INDEX, so an index-type prefix needs a trailing space: index('FULLTEXT '), not index('FULLTEXT').

Column helpers

$table->id(); // id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY
$table->increments('sort'); // BIGINT sort NOT NULL AUTO_INCREMENT
$table->string('email'); // VARCHAR(255)
$table->timestamps(); // nullable created_at + updated_at TIMESTAMPs
$table->softDeletes(); // nullable deleted_at TIMESTAMP

id() is shorthand for $table->bigint('id')->unsigned()->increments()->primary(). increments() without a name argument applies AUTO_INCREMENT to the most recently defined column.

timestamps() and softDeletes() create the columns that the model-side public $timestamps = true and public $soft_deletes = true properties depend on. Soft-delete models exclude trashed rows from reads by default; opt out with public $soft_delete_scope = false.

Foreign keys

Declare constraints by chaining on the column that holds the foreign key value:

$table->bigint('user_id')->unsigned()
->foreign('users', 'id') // references users(id)
->onDelete()->cascade() // ON DELETE CASCADE
->onUpdate()->restrict(); // ON UPDATE RESTRICT
MethodDescription
foreign($ref, $refCol)FOREIGN KEY from the current column to $refCol on table $ref. $ref is used as-is; pass the fully prefixed table name.
onDelete()Selects the ON DELETE slot for the next action.
onUpdate()Selects the ON UPDATE slot for the next action.
cascade()Applies CASCADE. Without a preceding onDelete() / onUpdate(), both slots are set.
restrict()Applies RESTRICT. Same scoping rules as cascade().
setNull()Applies SET NULL. Same scoping rules as cascade().

Altering tables

Schema::edit('orders', function ($table) {
$table->varchar('reference', 64)->nullable(); // ADD COLUMN
$table->tinyint('priority')->defaultValue(0); // ADD COLUMN
$table->dropColumn('legacy_notes'); // DROP COLUMN
$table->dropTimestamps(); // DROP created_at + updated_at
});

Chain change() to emit MODIFY COLUMN instead of ADD COLUMN:

Schema::edit('orders', function ($table) {
$table->varchar('reference', 128)->change(); // widen the column
});

Drop helpers

MethodEmitted SQL
dropColumn($column)DROP $column
dropTimestamps()DROP COLUMN created_at, DROP COLUMN updated_at
dropIndex($indexes)DROP INDEX; accepts a name string or an array of names
dropUnique($indexes)Same as dropIndex; unique indexes are dropped by name
dropForeign($keys)DROP FOREIGN KEY; accepts a name string or an array
dropPrimary()DROP PRIMARY KEY
warning

dropIndex() and dropUnique() write to the same internal slot, so calling both inside one Schema::edit() silently discards the first. Drop them in separate edit() calls, or pass all index names to a single dropIndex().

Table operations

Schema::create('orders', $callback); // CREATE TABLE IF NOT EXISTS
Schema::edit('orders', $callback); // ALTER TABLE
Schema::drop('orders'); // DROP TABLE IF EXISTS
Schema::rename('orders', 'sales_orders'); // ALTER TABLE ... RENAME TO
Schema::addColumn('orders', 'note', 'TEXT');
Schema::dropColumn('orders', 'note');
Schema::renameColumn('orders', 'fname', 'first_name');

Every method works as a static call or on an instance ((new Schema())->create(...)), and any of them can be prefixed with withPrefix() / withWpPrefix().

note

renameColumn() emits RENAME COLUMN old TO new, which requires MySQL 8.0+. Older MySQL / MariaDB versions need the full column definition with CHANGE; use unsafeRaw() there.

Blueprint execution

Schema::create() and friends build a Blueprint and run it in one call, so you rarely touch these. If you drive a Blueprint yourself, note that all three run the DDL:

MethodBehavior
build()Alias of toSql()
toSql()Compiles and executes the statement; it does not return SQL
execute()Runs the compiled statement, restores $wpdb error state, throws RuntimeException on failure
warning

Despite the name, Blueprint::toSql() executes the DDL. There is no dry-run or SQL-preview API in the schema builder.

Schema::createBlueprint($table, $method, $callback) and Schema::build($blueprint) are the facade's own plumbing; renameColumnQuery() compiles the rename fragment used inside edit().

Error handling

Unlike the query builder, schema operations re-throw database errors as RuntimeException:

try {
Schema::create('orders', $callback);
} catch (\RuntimeException $e) {
error_log($e->getMessage());
}