Models

Fuwafuwa Framework provides a robust Model layer to simplify database interactions. Models offer a convenient interface for common database operations, such as retrieving, updating, deleting, and creating records.

Models are the heart of your application's data management. They encapsulate all logic related to data storage, retrieval, and manipulation, providing a clean and consistent interface for working with your database. In Fuwafuwa, models are designed to be intuitive and powerful, abstracting away the complexity of raw SQL queries while still providing full control when needed.

Whether you're building a simple blog or a complex e-commerce platform, the Model layer in Fuwafuwa will save you time and effort by providing a set of tools for common database operations. From basic CRUD (Create, Read, Update, Delete) operations to advanced query building and validation, Fuwafuwa's models have you covered.

Creating a Model

Creating a model in Fuwafuwa is simple and straightforward. Models are typically located in the app/controllers/user/model directory, where they're easily accessible by your controllers.

Each model in Fuwafuwa represents a database table and encapsulates all the logic for working with that table. By extending the \Fuwafuwa\BaseModel class, your models inherit a rich set of methods for performing common database operations.

A basic model structure looks like this:

<?php

namespace Model;
    
class User extends \Fuwafuwa\BaseModel {

    function __construct(\Fuwafuwa\Db $db) {
        parent::__construct($db, 'user', [
            'ai_field' => 'id',
            'primary_keys' => 'id',
            'created_field' => 'created',
            'modified_field' => 'updated',
            'deleted_field' => 'deleted',
            'except_fields' => ['token', 'push_token'],
        ]);
        
        // Validation rules (using Rakit Validation)
        // $this->validation = [ 
        //   'rules' => [],
        //   'custom_message' => []
        // ];    
        
        // Data preprocessing
        // $this->preProcess = function(array $data): array {
        //   return $data;
        // };    

        // Lifecycle hooks
        // $this->preCreate = function(array $self, array $pkeys): void { };    
        // $this->postCreate = function(array $self, array $pkeys): void { };    
    }
}
💡 Code Generator

Fuwafuwa Framework simplifies model creation through the CLI code generator:

php index.php cli/codegenerator/model --name=User --table=users > app/controllers/user/model/user.php

See Code Generator documentation for more options including validation rules.

Model Configuration

The model constructor in Fuwafuwa accepts configuration options that are converted to a type-safe ModelConfiguration value object. This provides a flexible way to define table structure, field behavior, and other important settings with IDE autocompletion support.

By configuring your model properly, you can automate common tasks such as timestamp management, soft deletes, and field filtering, which simplifies your code and reduces the chance of errors.

The constructor accepts these configuration options:

OptionTypeDescription
ai_field string Auto-increment field name. If set and no primary_keys specified, becomes the primary key.
primary_keys string Comma-separated primary key field names. Required unless ai_field is set.
created_field string Automatically populated with current timestamp on insert.
modified_field string Automatically updated with current timestamp on modification.
deleted_field string Enables soft deletes. Sets timestamp instead of permanently deleting.
except_fields array Fields to exclude from save operations (e.g., ['token', 'password']).
only_fields array If set, only these fields will be saved. Takes precedence over except_fields.
cache_ttl int Default cache TTL in seconds (default: 60).

Accessing Configuration

You can access the model's configuration at runtime:

$config = $user->getConfiguration();

// Access configuration properties
$aiField = $config->aiField;           // 'id'
$primaryKeys = $config->primaryKeys;    // 'id'
$tableName = $user->getTableName();     // 'user'

Field Filtering

For security, control which request values are accepted by configuring field filtering in the constructor:

// Exclude specific fields from save operations
parent::__construct($db, 'user', [
    'primary_keys' => 'id',
    'except_fields' => ['token', 'push_token', 'secret_key'],
]);

// Or only allow specific fields
parent::__construct($db, 'user', [
    'primary_keys' => 'id',
    'only_fields' => ['name', 'email', 'role'],
]);
⚠️ Deprecated

The old setConfig() method for field filtering has been removed. Use constructor options instead.

Model Methods

BaseModel provides these utility methods for working with model data:

MethodDescription
getConfiguration()Returns the ModelConfiguration value object
getPrimaryKeyFields()Returns array of primary key field names
getPrimaryKeyValues()Returns array of current primary key values
getLastInsertId()Returns the last auto-increment ID
getTableName()Returns the full table name with prefix
retrieve($keyValues)Load record by primary key(s), returns null if not found
retrieveOrFail($keyValues)Load record by primary key(s), throws exception if not found
copyExcept($fields, $data)Copy data excluding specified fields
copyOnly($fields, $data)Copy only specified fields from data
saveFromData($data)Save from array or HIVE key, respecting field filters
saveFromRequest()Save from REQUEST, respecting field filters
paginateRecords($pos, $size, ...)Returns PaginationResult with subset, total, pages
getByField($field, $value, $limit)Get records by field value
getCodeValuePairs($code, $value, ...)Get array for dropdown options
isValueUnique($field, $value)Check if a field value is unique

Retrieving Records

$user = m(\Model\User::class);

// Retrieve by primary key(s)
$user->retrieve([123]);
if (!$user->dry()) {
    echo $user->name;
}

// Retrieve or throw exception
$user = m(\Model\User::class);
$user->retrieveOrFail([123]);  // Throws ModelNotFoundException if not found

// Get primary key information
$keys = $user->getPrimaryKeyFields();    // ['id']
$values = $user->getPrimaryKeyValues();  // [123]

Pagination

$user = m(\Model\User::class);
$result = $user->paginateRecords(
    pos: 0,           // Current page (0-indexed)
    size: 20,         // Items per page
    queryFields: '*', // Fields to select
    order: 'name ASC',
    condition: 'role = "admin"'
);

// Access pagination results
$users = $result->records;  // Array of user data
$total = $result->total;    // Total record count
$pages = $result->count;    // Total pages
$current = $result->pos;    // Current page

Validation

Data validation is a crucial aspect of any web application, ensuring that the data entered by users meets specific requirements before it's stored in the database. Fuwafuwa makes this process easy and efficient with built-in support for Rakit Validation, a powerful and flexible validation library for PHP.

With Rakit Validation, you can define complex validation rules in a simple and readable format. The library provides a wide range of built-in validation rules, from basic checks like "required" and "email" to more advanced rules like "unique" (checking if a value exists in a database table) and "same" (checking if two fields match).

Here's how you define validation rules in your Fuwafuwa model:

$this->validation = [
    'rules' => [
        'login' => 'required|alpha_num|unique:login',
        'fullname' => 'required',
        'password1' => fn (string $action, array $data): string => 
            preg_match(static::REG_ADD, $action) 
                ? 'required|min:6' 
                : 'nullable|min:6',
        'password2' => 'present|same:password1',
        'role' => 'required',
    ]
];
💡 Note on Virtual Fields

The password1 and password2 fields are virtual (not in the database). They're used for password validation during input, then hashed in preProcess before storage.

PreProcess

The preProcess callback is a powerful feature in Fuwafuwa models that allows you to modify and prepare data before it's inserted or updated in the database. This is particularly useful for tasks like data sanitization, password hashing, or modifying field values based on specific conditions.

PreProcess callbacks are executed automatically before every save operation (both insert and update), ensuring that your data is always properly formatted and validated before it reaches the database. This helps maintain data consistency and reduces the chance of errors.

Here's an example of how you might use the preProcess callback:

$this->preProcess = function (array $data): array {
    if ($data['password1']) {
        $data['password'] = password_hash($data['password1'], PASSWORD_BCRYPT);
    }
    $data['fullname'] = trim($data['fullname']);
    return $data;
};

Lifecycle Events

Lifecycle events are another powerful feature of Fuwafuwa models. They allow you to attach custom code to specific moments in a record's lifecycle, such as before it's created, after it's updated, or before it's deleted. This provides a clean and modular way to add behavior to your models.

These events are particularly useful for tasks like sending notifications, logging activities, clearing caches, or performing additional database operations when a record is modified. By using lifecycle events, you can keep your code organized and maintainable.

Fuwafuwa provides seven events for each CRUD operation, plus a special postView event:

EventTimingUse Case
preCreateBefore insertSet default values, generate tokens
postCreateAfter insertSend notifications, log activity
preUpdateBefore updateValidate changes, modify data
postUpdateAfter updateClear cache, notify users
preDeleteBefore deleteCheck dependencies, backup data
postDeleteAfter deleteClean up related files
postViewAfter loadIncrement view counts, log access

Example: Setting a Token

$this->preCreate = function(array $self, array $pkeys): void {
    $self['token'] = md5((string) time());
};

Working with Data

Once you've created your model, working with data in Fuwafuwa is simple and intuitive. The framework provides a set of helper functions and methods that make it easy to perform common database operations.

One of the key features of Fuwafuwa models is that they are managed through the dependency injection container, which simplifies instantiation and makes your code more testable.

Use the m() helper function to get a model instance via Dependency Injection:

$user = m(\Model\User::class);

Insert or Update Pattern

$user = m(\Model\User::class);
$user->retrieve([$userId]);
if($user->dry()) {
    // No record - create new
    $user->copyfrom('GET');
    $user->created = gmdate('Y-m-d');
} else {
    // Has record - update
    $user->copyfrom('GET');
    $user->updated = gmdate('Y-m-d');
}
$user->save();

Key Methods

MethodDescription
retrieve($keys)Load record by key(s)
dry()Check if no record was loaded
copyfrom($source)Copy data from request or array
save()Insert or update record
erase()Delete record (or soft delete)
loaded()Check if record was loaded
💡 Dependency Injection

Fuwafuwa uses the Dice library to automatically resolve dependencies. Use the c() utility function to get service instances:

// Get a service instance
$queueManager = c(QueueManager::class);
$user = c('\Model\User');

Migration Notes

Recent versions of Fuwafuwa Framework have modernized the BaseModel API. If you're working with older code, here are the key changes:

Old Way (Deprecated)New Way
$model->getConfig('ai_field') $model->getConfiguration()->aiField
$model->setConfig('except_fields', [...]) Pass in constructor opts
$model->pk() $model->getPrimaryKeyFields()
$model->pkValues() $model->getPrimaryKeyValues()
$model->lastInsertId() $model->getLastInsertId()
$model->unique($field, $value) $model->isValueUnique($field, $value)
$model->tableName() $model->getTableName()
$model->page($pos, $size, ...) $model->paginateRecords($pos, $size, ...)