Schema Builder
Introduction
Schema is a facade over Blueprint for creating, altering and dropping MySQL tables from PHP.
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.
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
| Method | SQL 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
| Method | SQL 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
| Method | SQL type | Notes |
|---|---|---|
bit($name, $length = null) | BIT | |
tinyint($name, $length = null) | TINYINT | |
bool($name) | BOOL | |
boolean($name) | BOOLEAN | Alias of bool |
smallint($name, $length = null) | SMALLINT | |
mediumint($name, $length = null) | MEDIUMINT | |
int($name, $length = null) | INT | |
integer($name, $length = null) | INTEGER | Alias of int |
bigint($name, $length = null) | BIGINT | |
float($name, $length = null) | FLOAT | |
double($name, $length = null) | DOUBLE | |
double_precision($name, $length = null) | DOUBLE PRECISION | Underscore maps to a space |
decimal($name, $length = null, $scale = null) | DECIMAL | Only decimal / dec accept a third argument |
dec($name, $length = null, $scale = null) | DEC | Alias of decimal |
Enumerated
$table->enum('status', ['draft', 'published', 'archived']);
$table->set('permissions', ['read', 'write', 'delete']);
Date / time
| Method | SQL 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();
| Modifier | Effect |
|---|---|
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. |
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
| Method | Description |
|---|---|
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
| Method | Emitted 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 |
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().
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:
| Method | Behavior |
|---|---|
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 |
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());
}