Milk Admin

RuleBuilder - Schema Definition

Revision: 2025/10/13

The RuleBuilder class provides a fluent interface for defining your database schema, validation rules, and relationships in the Model's configure() method. It handles table structure, field types, constraints, form generation, and relationships all in one place.

💡 What RuleBuilder Does:
  • Database Schema: Defines table structure and field types
  • Validation Rules: Sets required fields, constraints, and validation
  • Form Generation: Automatically generates form fields from schema
  • Relationships: Defines hasOne, belongsTo, hasMany relationships
  • Display Control: Controls visibility in lists, forms, and views

Basic Usage

RuleBuilder is used inside your Model's configure() method:

class ProductsModel extends AbstractModel
{
    protected function configure($rule): void {
        // $rule is a RuleBuilder instance
        $rule->table('#__products')
            ->id()                                    // Auto-increment primary key
            ->string('name', 100)->required()         // VARCHAR(100) NOT NULL
            ->decimal('price', 10, 2)->default(0)     // DECIMAL(10,2) DEFAULT 0
            ->text('description')->nullable()         // TEXT NULL
            ->boolean('in_stock')->default(true)      // TINYINT(1) DEFAULT 1
            ->datetime('created_at')->nullable();     // DATETIME NULL
    }
}

Configuration Methods

table(string $name): self

Sets the database table name. Use #__ prefix which will be replaced with the configured database prefix.

$rule->table('#__products');
// Creates table: prefix_products

$rule->table('#__orders');
// Creates table: prefix_orders

db(string $type): self

Sets which database connection to use (default: 'db', or 'db2' for second database).

$rule->table('#__logs')
    ->db('db2');  // Use secondary database

renameField(string $from, string $to): self

Declares a rename operation during schema updates (use with buildTable()).

$rule->table('#__users')
    ->string('name', 150)
    ->renameField('full_name', 'name');

Field Type Methods

Primary Key

id(string $name = 'id'): self

Creates an auto-increment primary key field.

// Standard primary key
$rule->id();  // Creates 'id' INT AUTO_INCREMENT PRIMARY KEY

// Custom name
$rule->id('product_id');  // Creates 'product_id' as primary key

String Fields

string(string $name, int $length = 255): self

Creates a VARCHAR field.

// Basic string
$rule->string('name', 100);  // VARCHAR(100)

// With constraints
$rule->string('username', 50)
    ->required()
    ->unique();

// With default value
$rule->string('status', 20)
    ->default('active');

text(string $name): self

Creates a TEXT field for long content.

$rule->text('description');
$rule->text('content')->nullable();

email(string $name): self

Creates an email field with automatic email validation.

$rule->email('user_email')
    ->required();

tel(string $name): self

Creates a telephone field (VARCHAR 25).

$rule->tel('phone')
    ->nullable();

url(string $name): self

Creates a URL field with validation.

$rule->url('website')
    ->nullable();

Numeric Fields

int(string $name): self

Creates an INTEGER field.

// Basic integer
$rule->int('quantity');

// With constraints
$rule->int('age')
    ->min(0)
    ->max(150);

// Unsigned integer
$rule->int('views')
    ->unsigned()
    ->default(0);

// Foreign key with index
$rule->int('category_id')
    ->index();

decimal(string $name, int $length = 10, int $precision = 2): self

Creates a DECIMAL field for precise numeric values (e.g., prices, measurements).

// Price field: 10 total digits, 2 decimal places
$rule->decimal('price', 10, 2)
    ->default(0)
    ->min(0);

// Weight: 8 total digits, 3 decimal places
$rule->decimal('weight', 8, 3)
    ->nullable();

// Percentage: 5 total digits, 2 decimal places
$rule->decimal('discount_rate', 5, 2)
    ->min(0)
    ->max(100);

Boolean Fields

boolean(string $name): self / checkbox(string $name): self

Creates a boolean/checkbox field (TINYINT 1).

$rule->boolean('is_active')
    ->default(true);

$rule->checkbox('accept_terms')
    ->required();

Date & Time Fields

datetime(string $name): self

Creates a DATETIME field (format: Y-m-d H:i:s).

$rule->datetime('created_at')
    ->nullable();

$rule->datetime('updated_at')
    ->default('CURRENT_TIMESTAMP');

date(string $name): self

Creates a DATE field (format: Y-m-d).

$rule->date('birth_date')
    ->nullable();

$rule->date('published_at');

time(string $name): self

Creates a TIME field (format: H:i:s).

$rule->time('opening_time')
    ->nullable();

Selection Fields

list(string $name, array $options): self / select(string $name, array $options): self

Creates a dropdown/select field.

// Basic dropdown
$rule->list('status', [
    'draft' => 'Draft',
    'published' => 'Published',
    'archived' => 'Archived'
])->default('draft');

// From Model
$rule->list('category_id', (new CategoriesModel())->getList())
    ->required();

enum(string $name, array $options): self

Creates an ENUM field in the database.

$rule->enum('priority', [
    'low' => 'Low Priority',
    'medium' => 'Medium Priority',
    'high' => 'High Priority'
])->default('medium');

radio(string $name, array $options): self

Creates radio buttons.

$rule->radio('gender', [
    'M' => 'Male',
    'F' => 'Female',
    'O' => 'Other'
]);

checkboxes(string $name, array $options): self

Creates multiple checkboxes (stores as array).

$rule->checkboxes('features', [
    'wifi' => 'WiFi',
    'parking' => 'Parking',
    'pool' => 'Swimming Pool'
]);

File Upload Fields

file(string $name): self

Creates a file upload field.

// Single file
$rule->file('document')
    ->accept('.pdf,.doc,.docx')
    ->maxSize(5 * 1024 * 1024)  // 5MB
    ->uploadDir('uploads/documents');

// Multiple files
$rule->file('attachments')
    ->multiple()
    ->maxFiles(5)
    ->accept('.pdf,.jpg,.png');

image(string $name): self

Creates an image upload field (automatically accepts image/* types).

// Single image
$rule->image('profile_picture')
    ->maxSize(2 * 1024 * 1024)  // 2MB
    ->uploadDir('uploads/profiles');

// Multiple images
$rule->image('gallery')
    ->multiple()
    ->maxFiles(10)
    ->uploadDir('uploads/gallery');

Constraint Methods

required(): self

Makes the field required (NOT NULL in database + form validation).

$rule->string('name', 100)
    ->required();

nullable(bool $nullable = true): self

Allows NULL values (opposite of required).

$rule->string('middle_name', 50)
    ->nullable();

default($value): self

Sets default value for the field.

$rule->boolean('is_active')
    ->default(true);

$rule->string('status', 20)
    ->default('pending');

$rule->int('views')
    ->default(0);

unique(): self

Creates a UNIQUE constraint in the database.

$rule->string('username', 50)
    ->required()
    ->unique();

$rule->email('user_email')
    ->unique();

index(): self

Creates a database index for faster queries.

// Foreign keys should have indexes
$rule->int('category_id')
    ->index();

// Frequently searched fields
$rule->string('sku', 50)
    ->index();

unsigned(): self

Makes numeric fields unsigned (only positive values).

$rule->int('quantity')
    ->unsigned()
    ->default(0);

Numeric Constraints

// min($value): Minimum value
$rule->int('age')
    ->min(18);

// max($value): Maximum value
$rule->int('quantity')
    ->max(9999);

// step($value): Step increment (for HTML5 input)
$rule->decimal('price', 10, 2)
    ->step(0.01)
    ->min(0);

Display Control Methods

hideFromList(): self

Hides field from table/list views.

$rule->text('long_description')
    ->hideFromList();  // Don't show in lists

hideFromEdit(): self

Hides field from edit forms.

$rule->datetime('created_at')
    ->hideFromEdit();  // Auto-set, don't allow editing

hideFromView(): self

Hides field from detail/view pages.

$rule->string('password_hash', 255)
    ->hideFromView();  // Never display passwords

excludeFromDatabase(): self

Field won't be created in database (virtual/computed fields).

$rule->string('full_name', 255)
    ->excludeFromDatabase()  // Computed from first_name + last_name
    ->getter(function($obj) {
        return $obj->first_name . ' ' . $obj->last_name;
    });

Form Customization

label(string $label): self

Sets the display label for the field.

$rule->string('user_email', 100)
    ->label('Email Address')
    ->required();

formType(string $type): self

Sets the HTML5 input type.

$rule->string('website', 255)
    ->formType('url');

$rule->string('phone', 25)
    ->formType('tel');

$rule->text('content')
    ->formType('textarea');

formLabel(string $label): self

Sets a different label specifically for forms.

$rule->string('pwd', 255)
    ->label('Password')           // Display label
    ->formLabel('Enter Password');  // Form-specific label

formParams(array $params): self

Sets custom form parameters (HTML attributes).

$rule->string('username', 50)
    ->formParams([
        'placeholder' => 'Enter username',
        'autocomplete' => 'username',
        'pattern' => '[a-zA-Z0-9]+',
        'minlength' => 3,
        'maxlength' => 50
    ]);

error(string $message): self

Sets custom validation error message.

$rule->string('username', 50)
    ->required()
    ->error('Username is required and must be 3-50 characters');

Relationship Methods

hasOne(string $alias, string $relatedModel, string $foreignKey, string $onDelete = 'CASCADE'): self

Defines a one-to-one relationship where the foreign key is in the RELATED table.

// Actor hasOne Biography (biography.actor_id references actor.id)
class ActorsModel extends AbstractModel {
    protected function configure($rule): void {
        $rule->table('#__actors')
            ->id()->hasOne('biography', BiographyModel::class, 'actor_id', 'CASCADE')
            ->string('name', 100)->required();
    }
}

// Access the relationship
$actor = $actorsModel->getById(1);
$bio = $actor->biography;  // Lazy loads BiographyModel
echo $bio->bio_text;

onDelete options:

  • CASCADE - Delete child record when parent is deleted
  • SET NULL - Set foreign key to NULL in child
  • RESTRICT - Prevent parent deletion if child exists

hasMany(string $alias, string $relatedModel, string $foreignKey, string $onDelete = 'CASCADE'): self

Defines a one-to-many relationship where the foreign key is in the RELATED table.

// Author hasMany Books (books.author_id references authors.id)
class AuthorsModel extends AbstractModel {
    protected function configure($rule): void {
        $rule->table('#__authors')
            ->id()->hasMany('books', BooksModel::class, 'author_id', 'CASCADE')
            ->string('name', 100)->required();
    }
}

// Access the relationship
$author = $authorsModel->getById(1);
$books = $author->books;  // Returns array of BooksModel instances
foreach ($books as $book) {
    echo $book->title . "\n";
}

belongsTo(string $alias, string $relatedModel, string $relatedKey = 'id'): self

Defines a many-to-one relationship where the foreign key is in THIS table.

// Post belongsTo User (posts.user_id references users.id)
class PostsModel extends AbstractModel {
    protected function configure($rule): void {
        $rule->table('#__posts')
            ->id()
            ->string('title', 200)->required()
            ->int('user_id')->belongsTo('author', UsersModel::class, 'id')
            ->text('content');
    }
}

// Access the relationship
$post = $postsModel->getById(1);
$author = $post->author;  // Lazy loads UsersModel
echo $author->name;

Advanced Customization

Custom Getters and Setters

// getter(callable $fn): Custom getter function
$rule->string('price_formatted', 50)
    ->excludeFromDatabase()
    ->getter(function($obj) {
        return '€' . number_format($obj->price, 2);
    });

// setter(callable $fn): Custom setter function
$rule->string('email', 255)
    ->setter(function($value) {
        return strtolower(trim($value));
    });

// rawGetter(callable $fn): Raw value getter
$rule->datetime('created_at')
    ->rawGetter(function($value) {
        return $value->format('Y-m-d H:i:s');
    });

property(string $key, $value): self

Sets a custom property on the field configuration.

$rule->string('code', 50)
    ->property('auto-generate', true)
    ->property('prefix', 'PRD-');

customize(callable $callback): self

Applies a custom callback to modify field configuration.

$rule->string('status', 20)
    ->customize(function($fieldConfig) {
        $fieldConfig['custom_validator'] = 'validateStatus';
        $fieldConfig['allow_transitions'] = ['draft' => 'published'];
        return $fieldConfig;
    });

Complete Example

class ProductsModel extends AbstractModel
{
    protected function configure($rule): void {
        $rule->table('#__products')

            // Primary Key
            ->id()

            // Basic Fields
            ->string('sku', 50)
                ->required()
                ->unique()
                ->index()
                ->label('Product SKU')

            ->string('name', 200)
                ->required()
                ->label('Product Name')

            ->text('description')
                ->nullable()
                ->hideFromList()

            // Numeric Fields
            ->decimal('price', 10, 2)
                ->required()
                ->min(0)
                ->step(0.01)
                ->default(0)

            ->int('stock_quantity')
                ->unsigned()
                ->default(0)
                ->min(0)

            ->decimal('weight', 8, 3)
                ->nullable()
                ->label('Weight (kg)')

            // Selection Fields
            ->list('status', [
                'draft' => 'Draft',
                'active' => 'Active',
                'discontinued' => 'Discontinued'
            ])->default('draft')

            ->list('category_id', (new CategoriesModel())->getList())
                ->required()
                ->index()

            // Relationships
            ->int('brand_id')
                ->index()
                ->belongsTo('brand', BrandsModel::class, 'id')

            ->id()
                ->hasMany('images', ProductImagesModel::class, 'product_id', 'CASCADE')
                ->hasMany('reviews', ProductReviewsModel::class, 'product_id', 'CASCADE')

            // Boolean Fields
            ->boolean('featured')
                ->default(false)

            ->boolean('is_active')
                ->default(true)

            // File Upload
            ->image('main_image')
                ->nullable()
                ->maxSize(5 * 1024 * 1024)
                ->uploadDir('uploads/products')
                ->accept('image/*')

            // Date Fields
            ->datetime('created_at')
                ->nullable()
                ->hideFromEdit()

            ->datetime('updated_at')
                ->nullable()
                ->hideFromEdit()

            // Virtual Field (computed)
            ->string('price_formatted', 50)
                ->excludeFromDatabase()
                ->hideFromEdit()
                ->getter(function($obj) {
                    return '€' . number_format($obj->price, 2);
                });
    }
}

Field Types Reference

Method Database Type Description
id($name)INT AUTO_INCREMENTAuto-increment primary key
string($name, $len)VARCHAR($len)Variable length string
text($name)TEXTLong text content
email($name)VARCHAR(255)Email with validation
tel($name)VARCHAR(25)Telephone number
url($name)VARCHAR(255)URL with validation
int($name)INTInteger number
decimal($n, $l, $p)DECIMAL($l,$p)Precise decimal number
boolean($name)TINYINT(1)True/False value
datetime($name)DATETIMEDate and time
date($name)DATEDate only
time($name)TIMETime only
list($name, $opts)variesDropdown select
enum($name, $opts)ENUMDatabase enum
radio($name, $opts)variesRadio buttons
checkboxes($n, $o)TEXT/JSONMultiple selection
file($name)VARCHAR(255)File upload
image($name)VARCHAR(255)Image upload

Tests

RuleBuilder has a dedicated unit test suite: php vendor/bin/phpunit tests/Unit/Builders/RuleBuilderMethodsTest.php

See Also

Loading...