Modern Active Record implementation for elegant database interaction using object-oriented patterns.
- Introduction
- Basic Usage
- Creating ORM Models
- CRUD Operations
- Query Builder
- Relationships
- Advanced Features
- Validation
- Best Practices
The ORM (Object-Relational Mapping) provides an Active Record implementation that allows you to interact with your database using object-oriented syntax. Each database table has a corresponding Model class.
- Fluent query builder interface
- Automatic relationship handling
- Built-in validation
- Soft deletes support
- Automatic timestamps
- Mass assignment protection
- Attribute casting
- Model events
- Collection class for results
<?php
namespace App\Model;
use System\Framework\Orm;
class User extends Orm {
protected static $table = 'users';
protected static $primaryKey = 'id';
protected static $timestamps = true;
protected static $fillable = [
'name',
'email',
'password',
];
protected static $hidden = [
'password',
];
protected static $casts = [
'id' => 'int',
'is_active' => 'bool',
];
}// Get all records
$users = User::all();
// Find by primary key
$user = User::find(1);
// Find or throw exception
$user = User::findOrFail(1);
// Check if record exists
$exists = User::where('email', $email)->exists();
// Count records
$count = User::where('is_active', true)->count();php easy make:model UserCreate a file in app/model/ directory:
File: app/model/User.php
<?php
namespace App\Model;
use System\Framework\Orm;
/**
* User Model
*
* @property int $id
* @property string $name
* @property string $email
* @property string $password
* @property int $status
* @property string $created_at
* @property string $updated_at
*/
class User extends Orm {
/**
* Table name
*/
protected static $table = 'users';
/**
* Primary key
*/
protected static $primaryKey = 'id';
/**
* Enable timestamps
*/
protected static $timestamps = true;
protected static $createdAtColumn = 'created_at';
protected static $updatedAtColumn = 'updated_at';
/**
* Fillable attributes for mass assignment
*/
protected static $fillable = [
'name',
'email',
'password',
'status',
];
/**
* Guarded attributes (cannot be mass assigned)
*/
protected static $guarded = [
'id',
];
/**
* Hidden attributes (excluded from toArray/toJson)
*/
protected static $hidden = [
'password',
];
/**
* Attribute casts
*/
protected static $casts = [
'id' => 'int',
'status' => 'int',
'is_verified' => 'bool',
];
/**
* Enable soft deletes
*/
protected static $softDelete = true;
protected static $deletedAtColumn = 'deleted_at';
/**
* Validation rules
*/
public function rules() {
return [
['name', 'required|string|minLength:2|maxLength:255'],
['email', 'required|email|maxLength:255'],
['password', 'required|string|minLength:8'],
];
}
/**
* Attribute labels for validation messages
*/
public function attributeLabels() {
return [
'name' => 'Full Name',
'email' => 'Email Address',
'password' => 'Password',
];
}
}$user = User::create([
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'secret123',
]);
echo $user->id; // Auto-assigned ID$user = new User();
$user->name = 'Jane Doe';
$user->email = 'jane@example.com';
$user->password = 'secret123';
$user->save();
echo $user->id;$user = new User();
$user->fill([
'name' => 'Bob Smith',
'email' => 'bob@example.com',
'password' => 'secret123',
]);
$user->save();$user = User::find(1);
if ($user) {
echo $user->name;
echo $user->email;
}$users = User::all();
foreach ($users as $user) {
echo $user->name;
}$user = User::where('email', $email)->first();$user = User::firstOrCreate(
['email' => 'user@example.com'],
['name' => 'New User', 'status' => 1]
);$user = User::updateOrCreate(
['email' => 'user@example.com'],
['name' => 'Updated Name', 'status' => 1]
);$user = User::find(1);
$user->name = 'Updated Name';
$user->email = 'updated@example.com';
$user->save();$affected = User::where('status', 0)
->update(['status' => 1]);// Increment column value
User::where('id', 1)->increment('login_count');
User::where('id', 1)->increment('points', 5);
// Decrement column value
User::where('id', 1)->decrement('credits');
User::where('id', 1)->decrement('balance', 10);$user = User::find(1);
$user->delete(); // Sets deleted_at timestamp$user = User::find(1);
$user->forceDelete(); // Permanently deletesUser::where('status', 0)->delete();$user = User::withTrashed()->find(1);
$user->restore();// Basic where
$users = User::where('status', 1)->get();
// Where with operator
$users = User::where('age', '>', 18)->get();
// Multiple where clauses
$users = User::where('status', 1)
->where('age', '>=', 18)
->get();
// OR where
$users = User::where('status', 1)
->orWhere('role', 'admin')
->get();
// Filter where (ignores null values)
$users = User::filterWhere('name', $name)
->filterWhere('email', $email)
->get();// WHERE IN
$users = User::whereIn('id', [1, 2, 3, 4, 5])->get();
// WHERE NOT IN
$users = User::whereNotIn('status', [0, 2])->get();// WHERE BETWEEN
$users = User::whereBetween('age', [18, 65])->get();
// WHERE NOT BETWEEN
$users = User::whereNotBetween('score', [0, 50])->get();// WHERE NULL
$users = User::whereNull('deleted_at')->get();
// WHERE NOT NULL
$users = User::whereNotNull('email_verified_at')->get();// WHERE date
$users = User::whereDate('created_at', '2025-01-01')->get();
// WHERE month
$users = User::whereMonth('created_at', 11)->get();
// WHERE year
$users = User::whereYear('created_at', 2025)->get();// ORDER BY ASC
$users = User::orderBy('name')->get();
// ORDER BY DESC
$users = User::orderBy('created_at', 'DESC')->get();
// Multiple ORDER BY
$users = User::orderBy('status', 'DESC')
->orderBy('name', 'ASC')
->get();// LIMIT
$users = User::limit(10)->get();
// LIMIT with OFFSET
$users = User::limit(10)->offset(20)->get();
// Take (alias for limit)
$users = User::take(5)->get();// GROUP BY
$stats = User::select(['role', 'COUNT(*) as count'])
->groupBy('role')
->get();
// GROUP BY with HAVING
$stats = User::select(['role', 'COUNT(*) as count'])
->groupBy('role')
->having('count', '>', 5)
->get();// INNER JOIN
$users = User::join('profiles', 'users.id', '=', 'profiles.user_id')
->select(['users.*', 'profiles.bio'])
->get();
// LEFT JOIN
$users = User::leftJoin('orders', 'users.id', '=', 'orders.user_id')
->get();// Count
$count = User::where('status', 1)->count();
// Max
$maxAge = User::max('age');
// Min
$minAge = User::min('age');
// Sum
$totalPoints = User::sum('points');
// Average
$avgAge = User::avg('age');// Select specific columns
$users = User::select(['id', 'name', 'email'])->get();
// Select with alias
$users = User::select(['name', 'email as user_email'])->get();// Get array of single column values
$names = User::pluck('name');
// Returns: ['John', 'Jane', 'Bob']
// Get associative array (key => value)
$users = User::pluck('name', 'id');
// Returns: [1 => 'John', 2 => 'Jane', 3 => 'Bob']// Process large datasets in chunks
User::chunk(100, function($users) {
foreach ($users as $user) {
// Process each user
echo $user->name;
}
});// Paginate results
$result = User::where('status', 1)
->orderBy('created_at', 'DESC')
->paginate(15);
// Access pagination data
$users = $result['data'];
$total = $result['total'];
$currentPage = $result['current_page'];
$lastPage = $result['last_page'];
$hasMore = $result['has_more_pages'];// In User model
public function profile() {
return $this->hasOne(Profile::class, 'user_id', 'id');
}
// Usage
$user = User::find(1);
$profile = $user->profile();// In User model
public function posts() {
return $this->hasMany(Post::class, 'user_id', 'id');
}
// Usage
$user = User::find(1);
$posts = $user->posts();// In Post model
public function user() {
return $this->belongsTo(User::class, 'user_id', 'id');
}
// Usage
$post = Post::find(1);
$user = $post->user();// In User model
public function roles() {
return $this->belongsToMany(
Role::class,
'user_roles', // Pivot table
'user_id', // Foreign key on pivot
'role_id', // Related key on pivot
'id', // Parent key
'id' // Related key
);
}
// Usage
$user = User::find(1);
$roles = $user->roles();// Load with relationships
$users = User::with('posts', 'profile')->get();
foreach ($users as $user) {
echo $user->name;
foreach ($user->posts as $post) {
echo $post->title;
}
}
// Nested eager loading
$users = User::with('posts.comments')->get();// Count related records without loading them
$users = User::withCount('posts')->get();
foreach ($users as $user) {
echo $user->posts_count; // Automatic attribute
}
// Multiple counts
$users = User::withCount('posts', 'comments')->get();
// Conditional count
$users = User::withCount([
'posts' => function($query) {
$query->where('published', 1);
}
])->get();// Join with relationship
$posts = Post::joinWith('user')->get();
// Left join with relationship
$posts = Post::joinWith('user', 'LEFT')->get();
// Join with constraints
$posts = Post::joinWith([
'user' => function($query) {
$query->where('status', 1);
}
])->get();// In Post model
public function user() {
return $this->belongsTo(User::class, 'user_id')
->inverseOf('posts');
}
// In User model
public function posts() {
return $this->hasMany(Post::class, 'user_id')
->inverseOf('user');
}// Enable in model
protected static $softDelete = true;
// Query including trashed
$users = User::withTrashed()->get();
// Query only trashed
$users = User::onlyTrashed()->get();
// Restore deleted
$user = User::withTrashed()->find(1);
$user->restore();protected static $casts = [
'id' => 'int',
'is_active' => 'bool',
'price' => 'float',
'metadata' => 'json',
'data' => 'array',
'created_at' => 'date',
];
// Usage - automatic casting
$user = User::find(1);
$isActive = $user->is_active; // Returns boolean, not 0/1Hidden Attributes
protected static $hidden = [
'password',
'remember_token',
];
// Converts to array without hidden fields
$array = $user->toArray();
$json = $user->toJson();// Whitelist attributes
protected static $fillable = [
'name',
'email',
];
// Or blacklist attributes
protected static $guarded = [
'id',
'is_admin',
];
// Usage
$user = User::create($request->post); // Only fillable attributes are setclass User extends Orm {
// Before saving (insert or update)
protected function beforeSave() {
// Modify attributes before save
if (isset($this->attributes['email'])) {
$this->attributes['email'] = strtolower($this->attributes['email']);
}
}
// After saving
protected function afterSave() {
// Log the save operation
$this->logger->info('User saved', ['id' => $this->id]);
}
// Before insert
protected function beforeInsert() {
// Generate default values
if (!isset($this->attributes['uuid'])) {
$this->attributes['uuid'] = uniqid();
}
}
// After insert
protected function afterInsert() {
// Send welcome email
$this->load->service('email|sendWelcome', $this);
}
// Before update
protected function beforeUpdate($changedAttributes) {
// Validate changes
}
// After update
protected function afterUpdate($changedAttributes) {
// Log what changed
$this->logger->info('User updated', $changedAttributes);
}
// Before delete
protected function beforeDelete() {
// Check if can be deleted
if ($this->posts_count > 0) {
return false; // Cancel deletion
}
}
// After delete
protected function afterDelete() {
// Cleanup related data
}
}// Find by raw SQL
$users = User::findBySql(
"SELECT * FROM users WHERE status = ? AND created_at > ?",
[1, '2025-01-01']
);
// Returns collection of User objects
foreach ($users as $user) {
echo $user->name;
}class User extends Orm {
public static function active() {
return static::where('status', 1);
}
public static function admins() {
return static::where('role', 'admin');
}
}
// Usage
$activeUsers = User::active()->get();
$admins = User::admins()->get();
$activeAdmins = User::active()->admins()->get();public function rules() {
return [
['name', 'required|string|minLength:2|maxLength:255'],
['email', 'required|email|maxLength:255'],
['password', 'required|string|minLength:8'],
['age', 'integer|min:18|max:120'],
['phone', 'optional|phone'],
];
}- required - Field must not be empty
- optional - Field can be empty
- email - Valid email format
- url - Valid URL format
- phone - Valid phone number
- uuid - Valid UUID format
- json - Valid JSON string
- string - Must be string
- integer - Must be integer
- numeric - Must be numeric
- boolean - Must be boolean
- minLength:n - Minimum string length
- maxLength:n - Maximum string length
- min:n - Minimum numeric value
- max:n - Maximum numeric value
- between:min,max - Value between range
- in:val1,val2 - Value in list
- notIn:val1,val2 - Value not in list
- regex:pattern - Match regex pattern
public function scenarios() {
return [
'register' => ['name', 'email', 'password'],
'update' => ['name', 'email'],
'admin' => ['name', 'email', 'role', 'status'],
];
}
// Usage
$user = new User();
$user->setScenario('register');
$user->fill($data);
if ($user->save()) {
// Validation passed
} else {
// Get validation errors
$errors = $user->getErrors();
}$user = new User();
$user->fill($data);
if ($user->validate()) {
// Validation passed
$user->save(false); // Skip validation since already done
} else {
// Get errors
$errors = $user->getErrors();
foreach ($errors as $field => $messages) {
echo "$field: " . implode(', ', $messages);
}
}namespace App\Model;
use System\Framework\Orm;
class User extends Orm {
// ...
}/**
* @property int $id
* @property string $name
* @property string $email
* @property bool $is_active
*/
class User extends Orm {
// ...
}// Always define both sides of relationship
class User extends Orm {
public function posts() {
return $this->hasMany(Post::class, 'user_id')->inverseOf('user');
}
}
class Post extends Orm {
public function user() {
return $this->belongsTo(User::class, 'user_id')->inverseOf('posts');
}
}// Good: N+1 query problem solved
$users = User::with('posts')->get();
foreach ($users as $user) {
foreach ($user->posts as $post) {
// No additional queries
}
}
// Bad: N+1 query problem
$users = User::all();
foreach ($users as $user) {
foreach ($user->posts() as $post) {
// Additional query for each user
}
}protected static $hidden = [
'password',
'remember_token',
'api_secret',
];- Models (Traditional) - Traditional database queries
- ORM Relationships - Detailed relationship guide
- Validation - Complete validation reference
- Query Builder - Advanced query building
Previous: Models (Traditional)
Next: Views