Milk Admin

Abstract Model - Overview

Revision: 2025/12/01

The AbstractModel class is the foundation for all data models in MilkAdmin. It provides a powerful and intuitive interface for interacting with database tables using a fluent query builder and comprehensive CRUD operations.

Table Structure Documentation: For complete information about defining table structures and using the RuleBuilder, see the RuleBuilder documentation.

Defining a Model

To create a model, extend AbstractModel and implement the configure() method where you define your table structure using a fluent interface:

namespace Modules\Products;
use App\Abstracts\AbstractModel;

class ProductsModel extends AbstractModel
{
    protected function configure($rule): void {
        $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
            ->int('category_id')->index()             // INT with INDEX
            ->created_at()                            // DATETIME
            ->datetime('updated_at')->nullable();     // DATETIME NULL
    }
}
⚠️ Important: The #__ prefix in table names is automatically replaced with the actual database prefix configured in your settings.

Recommended Directory Structure

Organize your module files following this pattern:

milkadmin_local/Modules/Products/
  ProductsModel.php       # Data model
  ProductsService.php     # Business logic
  ProductsModule.php      # Controller/routing
  Views/                  # Templates
    list.php
    edit.php

Common Field Types Quick Reference

Most frequently used field types:

// Text fields
$rule->string('NAME', 100)->required();
$rule->text('DESCRIPTION')->nullable();

// Numbers
$rule->int('QUANTITY')->default(0);
$rule->decimal('PRICE', 10, 2)->required();
$rule->bool('ACTIVE')->default(true);

// Dates and times
$rule->date('START_DATE')->formType('date');
$rule->datetime('EVENT_TIME')->formType('datetime');
$rule->time('OPENING_TIME')->formType('time');

// Special types
$rule->enum('STATUS', ['draft', 'published', 'archived'])->default('draft');
$rule->list('CATEGORY', ['A' => 'Category A', 'B' => 'Category B'])->formType('select');
$rule->array('TAGS')->default([]);

Common Modifiers

Chain multiple modifiers for complete field configuration:

$rule->string('EMAIL', 100)
    ->label('Email Address')
    ->required()
    ->unique()
    ->index()
    ->hideFromEdit()           // Hide in edit form
    ->formType('email');

Standalone Usage

You can also instantiate and use models directly:

use Modules\Products\ProductsModel;

$products = new ProductsModel();

// Simple query
$allProducts = $products->getAll();

// Query with conditions
$cheapProducts = $products
    ->where('price < ?', [50])
    ->order('price', 'asc')
    ->getResults();

Key Concepts

Model State and Immutability

⚠️ Important Behavior: Models hold query results internally. When you build a new query, you get a new Model instance with new results. The original model instance remains unchanged.
$model = new ProductsModel();
$allProducts = $model->getAll();        // Model with all products
$activeProducts = $model->where('active = ?', [1])->getResults();  // NEW Model, only active

// $allProducts still has all products - it hasn't changed!

Query Builder Pattern

Most query methods return a Query instance, allowing you to chain multiple operations. You must call getResults() or getRow() to execute the query and get back a Model:

// Query methods return Query instance for chaining
$results = $model
    ->where('status = ?', ['active'])     // Returns Query
    ->whereIn('category_id', [1, 2, 3])   // Returns Query
    ->order('created_at', 'desc')         // Returns Query
    ->limit(0, 10)                        // Returns Query
    ->getResults();                       // Executes query → Returns Model with multiple records

// For single record
$product = $model
    ->where('id = ?', [1])
    ->getRow();                           // Executes query → Returns Model with single record (isEmpty to check if record exists)
📖 Query Execution Methods:
  • getResults() - Executes the query and returns a Model with multiple records (even if 0 or 1)
  • getRow() - Executes the query and returns a Model with a single record, or null if not found
  • getVar() - Executes the query and returns a single value (useful for COUNT, SUM, etc.)

💡 Learn more: See Query Builder Methods for complete documentation.

Result Set Navigation

Query execution returns a Model instance containing the results. You can navigate through records using multiple approaches:

$products = $model->where('in_stock = ?', [true])->getResults();

// Approach 1: Iterator (foreach)
foreach ($products as $product) {
    echo $product->name . ": €" . $product->price . "\n";
}

// Approach 2: Manual navigation
while ($products->hasNext()) {
    echo $products->name;
    $products->next();
}

// Approach 3: Array access
$firstProduct = $products[0];
$secondProduct = $products[1];

Data Formatting

The Model provides three different data formats for flexible data handling:

$product = $model->getById(1);

// RAW format: DateTime objects, PHP arrays (default)
$rawDate = $product->created_at;              // Returns DateTime object
$product->setOutputMode('raw');               // Set mode permanently
echo $product->created_at->format('Y-m-d');   // 2024-01-15

// FORMATTED format: Human-readable strings
$formattedDate = $product->getFormattedValue('created_at');  // "15/01/2024 14:30"
$product->setOutputMode('formatted');         // Set mode permanently
echo $product->created_at;                    // "15/01/2024 14:30"

// SQL format: MySQL-compatible strings
$sqlDate = $product->getSqlValue('created_at');    // "2024-01-15 14:30:00"
$product->setOutputMode('sql');               // Set mode permanently

// Get all data in different formats
$allRaw = $products->getRawData('array', true);           // All records as arrays
$allFormatted = $products->getFormattedData('object', true);  // All records as objects
$allSql = $products->getSqlData('array', true);          // All records ready for SQL
💡 Tip: Use setOutputMode('formatted') when displaying data in views, setOutputMode('raw') for business logic, and setOutputMode('sql') when preparing data for manual SQL operations.

Two Ways to Save Data

1. Quick Method: store()

Direct save without validation - fast and simple:

// Insert new record
$id = $model->store([
    'name' => 'New Product',
    'price' => 29.99,
    'in_stock' => true
]);

// Update existing record
$model->store([
    'name' => 'Updated Name',
    'price' => 24.99
], $id);  // Pass ID as second parameter for UPDATE

2. Classic Method: fill() + validate() + save()

With full validation support:

// Get empty object or existing record
$product = $model->getEmpty($_POST);
// or
$product = $model->getById($id);

// Fill with data new
$product->fill([
    'name' => 'Product Name',
    'price' => 29.99
]);
// Or Change attribute name of current object
$product->name = 'Products';

if ($product->validate()) {
    if ($product->save()) {
        $id = $product->getLastInsertId();
        echo "Saved with ID: $id";
    } else {
        echo "Save error: " . $product->getLastError();
    }
} else {
    // Validation failed - errors in MessagesHandler
    echo "Validation failed";
}

Next Steps

💡 New Architecture: The Model has been completely refactored using traits for better organization and maintainability:
  • QueryBuilderTrait: Query building (where, whereIn, whereHas, order, limit)
  • CrudOperationsTrait: CRUD operations (getById, store, delete)
  • SchemaAndValidationTrait: Schema and validation (buildTable, validate)
  • RelationshipsTrait: Relationships (hasOne, belongsTo, hasMany)
  • CollectionTrait: Result set navigation and iteration
📚 Explore More:
Loading...