The Model API provides the base class for all database models in the Hoist PHP framework. It implements the Active Record pattern with comprehensive security features, multi-tenancy support, and robust data operations. All application models extend this class to inherit standard CRUD operations and advanced security filtering.
Location: Core/Libraries/Model.php
Pattern: Active Record with security enhancements
Usage: Extend this class for all application models
Features: CRUD operations, hidden field filtering, guard deletion, multi-tenant support
Type: Instance
Description: Application instance container for service access
Type: Database
Description: Primary database connection service (MySQL via Medoo ORM)
Type: FileDatabase
Description: File-based database service for structured data storage
Type: object
Description: All registered application models for cross-model operations
Type: object
Description: All registered application libraries for service access
Type: string|false
Required: Yes (must be set in child classes)
Description: Database table name associated with this model
Example:
class UserModel extends Model
{
public $table = 'users';
}$hiddenFields
Type: array
Default: []
Description: Fields to hide from query results (passwords, tokens, sensitive data)
Example:
class UserModel extends Model
{
public $table = 'users';
public $hiddenFields = ['password', 'reset_token', 'api_secret'];
}Type: array
Description: Query options for modifying behavior (method chaining support)
Internal Use: Managed automatically by the options() method
Initializes the model with application dependencies.
Parameters:
$instance(Instance): Application service container
Automatic Setup:
- Database connections (SQL and File)
- Access to all models and libraries
- Service container integration
Example:
// Models are automatically instantiated by the framework
// Access via controllers: $this->models->user->method()Model initialization hook for child classes.
Called: After construction
Override: Implement in child classes for custom initialization
Return: void
Example:
class UserModel extends Model
{
public $table = 'users';
public $hiddenFields = ['password', 'reset_token'];
public function instantiate()
{
// Set default ordering
$this->defaultOrder = ['created_at' => 'DESC'];
// Initialize custom properties
$this->validationRules = [
'email' => 'required|email|unique:users',
'password' => 'required|min:8'
];
}
}Sets options for modifying query behavior with method chaining.
Parameters:
$opts(array): Options to modify query behavior
Returns: self - For method chaining
Available Options:
includeHiddenFields: Include normally hidden fields in resultscustomWhere: Custom where conditionscustomOrder: Custom ordering
Example:
// Include hidden fields for admin users
$user = $this->models->user->options(['includeHiddenFields' => true])
->get(['id' => 123]);
// Method chaining with multiple options
$users = $this->models->user->options([
'includeHiddenFields' => true,
'customOrder' => ['last_login' => 'DESC']
])->getMany(['status' => 'active']);
// Temporary access to sensitive data
public function getUserForAdmin($userId)
{
return $this->models->user->options(['includeHiddenFields' => true])
->get($userId);
}Retrieves a single record from the database table.
Parameters:
$where(int|array|null): Filter conditionsint: Treated as ID lookup (WHERE id = $where)array: Complex where conditions for Medoonull: Throws exception (filter required)
$select(string): Fields to select (*for all visible fields)
Returns: array|null - Single record or null if not found
Throws: Exception - If table not set or where condition missing
Example:
// Get by ID
$user = $this->models->user->get(123);
// Get with complex where conditions
$user = $this->models->user->get([
'email' => 'user@example.com',
'status' => 'active'
]);
// Select specific fields
$userProfile = $this->models->user->get(123, 'id, name, email, created_at');
// Complex queries with Medoo syntax
$recentUser = $this->models->user->get([
'status' => 'active',
'created_at[>]' => date('Y-m-d', strtotime('-30 days')),
'ORDER' => ['created_at' => 'DESC']
]);
// Controller usage
public function showUser()
{
$userId = $this->router->param('id');
$user = $this->models->user->get($userId);
if (!$user) {
$this->response->sendError('User not found', 404);
return;
}
$this->response->sendJson($user);
}Retrieves multiple records from the database table.
Parameters:
$where(array|null): Filter conditions array for Medoo$select(string): Fields to select (*for all visible fields)
Returns: array - Array of records (empty array if none found)
Throws: Exception - If table not set, where condition missing, or invalid format
Example:
// Get all active users
$activeUsers = $this->models->user->getMany(['status' => 'active']);
// Get with ordering and limits
$recentUsers = $this->models->user->getMany([
'status' => 'active',
'ORDER' => ['created_at' => 'DESC'],
'LIMIT' => 10
]);
// Complex filtering
$premiumUsers = $this->models->user->getMany([
'AND' => [
'status' => 'active',
'subscription_type' => 'premium',
'created_at[>]' => date('Y-m-d', strtotime('-1 year'))
],
'ORDER' => ['last_login' => 'DESC']
]);
// Pagination support
public function getUsers()
{
$page = $this->request->get('page', 1);
$limit = $this->request->get('limit', 20);
$offset = ($page - 1) * $limit;
$users = $this->models->user->getMany([
'status' => 'active',
'ORDER' => ['created_at' => 'DESC'],
'LIMIT' => [$offset, $limit]
]);
$total = $this->models->user->count(['status' => 'active']);
$this->response->sendJson([
'users' => $users,
'pagination' => [
'current_page' => $page,
'total_pages' => ceil($total / $limit),
'total_records' => $total
]
]);
}Counts records in the database table.
Parameters:
$where(array|null): Filter conditions array for Medoo
Returns: int - Number of matching records
Throws: Exception - If table not set or where condition missing
Example:
// Count all active users
$activeCount = $this->models->user->count(['status' => 'active']);
// Count with complex conditions
$premiumCount = $this->models->user->count([
'AND' => [
'status' => 'active',
'subscription_type' => 'premium',
'created_at[>]' => date('Y-m-d', strtotime('-30 days'))
]
]);
// Analytics and reporting
public function getDashboardStats()
{
$stats = [
'total_users' => $this->models->user->count(['status[!]' => 'deleted']),
'active_users' => $this->models->user->count(['status' => 'active']),
'new_users_today' => $this->models->user->count([
'created_at[>]' => date('Y-m-d 00:00:00')
]),
'premium_users' => $this->models->user->count([
'subscription_type' => 'premium'
])
];
return $stats;
}Updates existing records in the database table.
Parameters:
$where(int|array): Update conditionsint: Update record with this IDarray: Complex where conditions for Medoo
$data(array): Associative array of field => value pairs to update
Returns: bool - True on success, false on failure
Throws: Exception - If table not set
Example:
// Update by ID
$success = $this->models->user->save(123, [
'name' => 'Updated Name',
'email' => 'new@example.com',
'updated_at' => date('Y-m-d H:i:s')
]);
// Update with complex where conditions
$success = $this->models->user->save([
'status' => 'pending',
'created_at[<]' => date('Y-m-d', strtotime('-7 days'))
], [
'status' => 'expired'
]);
// Controller usage with validation
public function updateUser()
{
$userId = $this->router->param('id');
$userData = $this->request->only(['name', 'email', 'phone']);
// Validate input
if (empty($userData['email']) || !filter_var($userData['email'], FILTER_VALIDATE_EMAIL)) {
$this->response->sendError('Invalid email address', 400);
return;
}
// Check if user exists
$existingUser = $this->models->user->get($userId);
if (!$existingUser) {
$this->response->sendError('User not found', 404);
return;
}
// Add timestamp
$userData['updated_at'] = date('Y-m-d H:i:s');
// Update user
if ($this->models->user->save($userId, $userData)) {
$updatedUser = $this->models->user->get($userId);
$this->response->sendJson($updatedUser);
} else {
$this->response->sendError('Failed to update user', 500);
}
}Creates new records in the database table.
Parameters:
$data(array): Associative array of field => value pairs to insert
Returns: mixed - ID of newly created record, or false on failure
Throws: Exception - If table not set
Example:
// Create new user
$userId = $this->models->user->create([
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => password_hash('secure123', PASSWORD_DEFAULT),
'status' => 'active',
'created_at' => date('Y-m-d H:i:s')
]);
// Controller usage with validation
public function createUser()
{
$userData = $this->request->only(['name', 'email', 'password']);
// Validate required fields
$errors = [];
if (empty($userData['name'])) {
$errors[] = 'Name is required';
}
if (empty($userData['email']) || !filter_var($userData['email'], FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Valid email is required';
}
if (empty($userData['password']) || strlen($userData['password']) < 8) {
$errors[] = 'Password must be at least 8 characters';
}
if (!empty($errors)) {
$this->response->sendError('Validation failed', 422, ['details' => $errors]);
return;
}
// Check for existing email
$existing = $this->models->user->get(['email' => $userData['email']]);
if ($existing) {
$this->response->sendError('Email already exists', 409);
return;
}
// Prepare user data
$userData['password'] = password_hash($userData['password'], PASSWORD_DEFAULT);
$userData['status'] = 'active';
$userData['created_at'] = date('Y-m-d H:i:s');
// Create user
$userId = $this->models->user->create($userData);
if ($userId) {
$newUser = $this->models->user->get($userId);
$this->response->sendJson($newUser, 201);
} else {
$this->response->sendError('Failed to create user', 500);
}
}Deletes records from the database table.
Parameters:
$where(int|array): Delete conditionsint: Delete record with this IDarray: Complex where conditions for Medoo
Returns: bool - True on success, false on failure
Throws: Exception - If table not set
Example:
// Delete by ID
$success = $this->models->user->delete(123);
// Delete with complex conditions
$success = $this->models->user->delete([
'status' => 'inactive',
'last_login[<]' => date('Y-m-d', strtotime('-2 years'))
]);
// Soft delete implementation
public function softDeleteUser($userId)
{
return $this->models->user->save($userId, [
'status' => 'deleted',
'deleted_at' => date('Y-m-d H:i:s')
]);
}
// Controller usage with authorization
public function deleteUser()
{
$userId = $this->router->param('id');
// Check authorization
if (!$this->instance->auth->hasPermission('delete_users')) {
$this->response->sendError('Insufficient permissions', 403);
return;
}
// Check if user exists
$user = $this->models->user->get($userId);
if (!$user) {
$this->response->sendError('User not found', 404);
return;
}
// Prevent self-deletion
if ($userId == $this->instance->auth->user['id']) {
$this->response->sendError('Cannot delete your own account', 400);
return;
}
// Use guard delete for safety
$result = $this->models->user->guardDelete([
[
'table' => 'posts',
'where' => ['author_id' => $userId],
'customTitle' => 'blog posts'
],
[
'table' => 'comments',
'where' => ['user_id' => $userId],
'customTitle' => 'user comments'
]
], $userId);
if ($result['success']) {
$this->response->sendSuccess(null, 'User deleted successfully');
} else {
$this->response->sendError($result['message'], 400, ['blocking' => $result['errors']]);
}
}Performs safe deletion with dependency checking and cleanup.
Parameters:
$guards(array): Dependency checks to perform$id(int): ID of the record to delete$associationsToDelete(array): Associated records to clean up
Returns: array - Response with success/error status and messages
Guard Array Structure:
[
'table' => 'table_name', // Table to check for dependencies
'where' => ['foreign_key' => $id], // Where conditions
'customTitle' => 'Custom Name' // Optional: custom name for errors
]Association Array Structure:
[
'table' => 'table_name', // Table containing associated records
'where' => ['foreign_key' => $id] // Where conditions for cleanup
]Example:
// Safe user deletion with dependency checking
public function safeDeleteUser($userId)
{
$result = $this->models->user->guardDelete([
// Check for blog posts
[
'table' => 'posts',
'where' => ['author_id' => $userId],
'customTitle' => 'blog posts'
],
// Check for active orders
[
'table' => 'orders',
'where' => [
'user_id' => $userId,
'status' => ['pending', 'processing']
],
'customTitle' => 'active orders'
],
// Check for admin roles
[
'table' => 'user_roles',
'where' => [
'user_id' => $userId,
'role' => 'admin'
],
'customTitle' => 'administrative roles'
]
], $userId, [
// Clean up user sessions
[
'table' => 'user_sessions',
'where' => ['user_id' => $userId]
],
// Clean up user preferences
[
'table' => 'user_preferences',
'where' => ['user_id' => $userId]
]
]);
return $result;
}
// Company deletion with complex dependencies
public function deleteCompany($companyId)
{
$result = $this->models->company->guardDelete([
// Check for active employees
[
'table' => 'users',
'where' => [
'company_id' => $companyId,
'status' => 'active'
],
'customTitle' => 'active employees'
],
// Check for pending projects
[
'table' => 'projects',
'where' => [
'company_id' => $companyId,
'status[!]' => 'completed'
],
'customTitle' => 'active projects'
],
// Check for outstanding invoices
[
'table' => 'invoices',
'where' => [
'company_id' => $companyId,
'status' => ['pending', 'overdue']
],
'customTitle' => 'unpaid invoices'
]
], $companyId, [
// Clean up company settings
[
'table' => 'company_settings',
'where' => ['company_id' => $companyId]
],
// Archive company documents
[
'table' => 'documents',
'where' => ['company_id' => $companyId]
]
]);
if ($result['success']) {
// Additional cleanup
$this->models->analytics->logCompanyDeletion($companyId);
$this->clearCompanyCache($companyId);
}
return $result;
}Hidden Field Filtering
Automatically removes sensitive fields from query results:
class UserModel extends Model
{
public $table = 'users';
public $hiddenFields = [
'password',
'reset_token',
'api_secret',
'two_factor_secret',
'remember_token'
];
}
// These fields are automatically stripped from results
$user = $this->models->user->get(123);
// Returns user data WITHOUT password, tokens, etc.
// Include hidden fields when needed (admin functions)
$userWithSecrets = $this->models->user->options(['includeHiddenFields' => true])
->get(123);
// Returns ALL fields including sensitive dataUses Medoo ORM with prepared statements:
// Safe parameterized queries
$users = $this->models->user->getMany([
'email[~]' => $searchTerm, // LIKE query with escaping
'status' => $status, // Exact match with escaping
'created_at[>]' => $date // Comparison with escaping
]);
// Complex safe queries
$results = $this->models->user->getMany([
'OR' => [
'name[~]' => $query,
'email[~]' => $query
],
'AND' => [
'status' => 'active',
'created_at[>]' => $startDate
]
]);Comprehensive error logging without data exposure:
// Errors are logged but not exposed to users
try {
$result = $this->models->user->create($userData);
} catch (Exception $e) {
// Error is logged in system logs
error_log("User creation failed: " . $e->getMessage());
// User receives generic error message
return false;
}class UserModel extends Model
{
public $table = 'users';
public $hiddenFields = ['password', 'reset_token', 'api_secret'];
public function instantiate()
{
$this->defaultOrder = ['created_at' => 'DESC'];
}
public function getByEmail($email)
{
return $this->get(['email' => $email]);
}
public function getActiveUsers()
{
return $this->getMany([
'status' => 'active',
'ORDER' => ['last_login' => 'DESC']
]);
}
public function createUser($userData)
{
// Hash password before storage
if (isset($userData['password'])) {
$userData['password'] = password_hash($userData['password'], PASSWORD_DEFAULT);
}
$userData['created_at'] = date('Y-m-d H:i:s');
$userData['status'] = 'active';
return $this->create($userData);
}
public function updateLastLogin($userId)
{
return $this->save($userId, [
'last_login' => date('Y-m-d H:i:s'),
'login_count' => $this->database->client->query(
"UPDATE users SET login_count = login_count + 1 WHERE id = ?",
[$userId]
)->rowCount()
]);
}
public function softDelete($userId)
{
return $this->save($userId, [
'status' => 'deleted',
'deleted_at' => date('Y-m-d H:i:s')
]);
}
public function getUserStats($userId)
{
$user = $this->get($userId);
if (!$user) return null;
return [
'user' => $user,
'post_count' => $this->models->post->count(['author_id' => $userId]),
'comment_count' => $this->models->comment->count(['user_id' => $userId]),
'last_activity' => $this->getLastActivity($userId)
];
}
}class PostModel extends Model
{
public $table = 'posts';
public $hiddenFields = ['draft_content'];
public function getPublished()
{
return $this->getMany([
'status' => 'published',
'published_at[<=]' => date('Y-m-d H:i:s'),
'ORDER' => ['published_at' => 'DESC']
]);
}
public function getByCategory($categoryId)
{
return $this->getMany([
'category_id' => $categoryId,
'status' => 'published',
'ORDER' => ['published_at' => 'DESC']
]);
}
public function getByAuthor($authorId)
{
return $this->getMany([
'author_id' => $authorId,
'ORDER' => ['created_at' => 'DESC']
]);
}
public function createPost($postData, $authorId)
{
$postData['author_id'] = $authorId;
$postData['created_at'] = date('Y-m-d H:i:s');
if ($postData['status'] === 'published' && !isset($postData['published_at'])) {
$postData['published_at'] = date('Y-m-d H:i:s');
}
return $this->create($postData);
}
public function publish($postId)
{
return $this->save($postId, [
'status' => 'published',
'published_at' => date('Y-m-d H:i:s')
]);
}
public function getPostWithAuthor($postId)
{
$post = $this->get($postId);
if (!$post) return null;
$author = $this->models->user->get($post['author_id'], 'id, name, email');
$post['author'] = $author;
return $post;
}
public function deletePost($postId)
{
// Use guard delete to check for comments
return $this->guardDelete([
[
'table' => 'comments',
'where' => ['post_id' => $postId],
'customTitle' => 'user comments'
]
], $postId, [
// Clean up post tags
[
'table' => 'post_tags',
'where' => ['post_id' => $postId]
]
]);
}
}class SensitiveModel extends Model
{
public $table = 'sensitive_data';
public $hiddenFields = [
'password',
'secret_key',
'private_token',
'sensitive_info'
];
}public function createRecord($data)
{
// Always validate input before database operations
if (empty($data['required_field'])) {
throw new Exception('Required field missing');
}
// Sanitize input
$data['email'] = filter_var($data['email'], FILTER_SANITIZE_EMAIL);
return $this->create($data);
}public function safeOperation($data)
{
try {
return $this->create($data);
} catch (Exception $e) {
// Log for debugging
error_log("Operation failed: " . $e->getMessage());
// Return user-friendly response
return false;
}
}// Use specific field selection for performance
$userList = $this->models->user->getMany(
['status' => 'active'],
'id, name, email' // Only select needed fields
);
// Use counting instead of full data retrieval
$userCount = $this->models->user->count(['status' => 'active']);The Model API seamlessly integrates with all framework components:
- Database: Primary MySQL operations via Medoo ORM
- FileDatabase: Alternative file-based storage support
- Authentication: User model integration with auth system
- Validation: Input validation support for data integrity
- Caching: Model-level caching for performance optimization
- Security: Built-in protection against SQL injection and data exposure
The Model API provides enterprise-grade data management with comprehensive security features and robust CRUD operations for scalable application development.