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.
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
}
}
#__ 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
$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)
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 foundgetVar()- 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
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
- 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
- Getting Started with Models - Beginner tutorial
- Query Builder Methods - Advanced queries
- CRUD Operations - Detailed CRUD documentation
- Relationships - hasOne, belongsTo, hasMany
- Schema - Table schema management
- Attributes - Manage operations of individual fields