Marwa\Framework\Database\Model is the framework-friendly base model built on top of marwa-db ORM. It keeps the upstream ORM behavior, then adds a small layer of convenience methods for common CRUD work such as table access, pagination, attribute normalization, create-or-update flows, and model state inspection.
Use it when you want an application model that feels concise in day-to-day development without hiding the underlying ORM.
The base class inherits the core ORM from marwa-db, then adds framework-level helpers such as:
newQuery()as a clean entry point for query buildingtableName()to read the resolved table nameuseConnection()to switch the model connectionnewInstance()for creating model instances programmaticallypaginate()that returns hydrated model instancesfirstWhere()andfindBy()for simple lookupsfirstOrCreate()andupdateOrCreate()for common persistence flowsexists()as an instance checkforceFill()for bypassing fillable protection deliberatelysyncOriginal(),isDirty(), andisClean()for change trackingfresh()for reloading the current recordsaveOrFail()anddeleteOrFail()for exception-based writes
That means you still get the ORM model behavior you expect, but the framework layer gives you a more ergonomic API for application code.
<?php
declare(strict_types=1);
namespace App\Models;
use Marwa\Framework\Database\Model;
final class User extends Model
{
protected static ?string $table = 'users';
protected static array $fillable = [
'name',
'email',
'active',
'meta',
];
protected static array $casts = [
'active' => 'bool',
'meta' => 'json',
];
}This is the standard pattern:
- set the table when it should not be inferred
- whitelist mass-assignable fields with
$fillable - define casts for attributes that need normalization
Use $table when you want to be explicit about the backing table:
protected static ?string $table = 'users';You can also read the resolved table name anywhere with:
User::tableName();Use $fillable to control which attributes may be mass assigned:
protected static array $fillable = [
'name',
'email',
'active',
];This matters for methods such as create(), fill(), and update() where arrays are applied to the model in bulk.
The framework model normalizes input attributes using the model cast map before persistence helpers run.
Supported framework-level normalization in this wrapper includes:
jsonandarrayboolintfloat
Example:
protected static array $casts = [
'active' => 'bool',
'meta' => 'json',
'login_count' => 'int',
'score' => 'float',
];Practical effect:
boolvalues are cast to booleansintvalues are cast to integersfloatvalues are cast to floatsjsonandarrayvalues are JSON-encoded when arrays or objects are provided
Use create() when you want to insert and return a model in one step:
$user = User::create([
'name' => 'Alice',
'email' => 'alice@example.com',
'active' => true,
'meta' => ['role' => 'admin'],
]);You can also create an instance first and save it later:
$user = User::newInstance([
'name' => 'Bob',
'email' => 'bob@example.com',
]);
$user->save();If you want an exception when the write fails, use:
$user->saveOrFail();For primary-key lookup:
$user = User::find(1);For a simple column lookup, use findBy() or firstWhere():
$user = User::findBy('email', 'alice@example.com');
$activeUser = User::firstWhere('active', true);When you need more control, drop into the query builder:
$users = User::newQuery()
->where('active', true)
->orderBy('name')
->get();newQuery() is just a convenience alias for query(), so use whichever reads better in your codebase.
These are useful when your code is driven by unique business keys such as email, slug, or external IDs.
If a matching row exists, it is returned. Otherwise, a new row is created.
$user = User::firstOrCreate(
['email' => 'alice@example.com'],
['name' => 'Alice', 'active' => true]
);If a matching row exists, it is updated with the supplied values. Otherwise, a new row is created.
$user = User::updateOrCreate(
['email' => 'alice@example.com'],
['name' => 'Alice Updated']
);This pattern is especially useful for sync jobs, import pipelines, and idempotent writes.
Once you have a model instance, update attributes and save:
$user = User::findBy('email', 'alice@example.com');
$user->fill([
'name' => 'Alice Smith',
]);
$user->save();Or bypass fillable checks intentionally with forceFill():
$user->forceFill([
'name' => 'Internal Override',
]);
$user->saveOrFail();forceFill() is useful for internal framework code, trusted imports, or maintenance jobs where mass-assignment rules should not apply.
Delete a loaded model in the usual way:
$user = User::find(1);
$user?->delete();If a failure should raise an exception:
$user->deleteOrFail();The framework model now exposes extra lifecycle hooks for audit trails on top of the ORM callbacks:
onRestoring()before a soft-deleted model is restoredonRestored()after a soft-deleted model is restoredonForceDeleting()before a hard deleteonForceDeleted()after a hard deleteonDestroying()before a bulkdestroy()onDestroyed()after a bulkdestroy()
Example:
User::onRestored(static function (User $user): void {
logger()->info('User restored', ['id' => $user->getKey()]);
});Use these hooks when you need an audit trail that covers restore and force-delete flows as well as the usual create, update, and delete paths.
The framework wrapper provides a paginate() helper that returns a structured array with hydrated model instances in data.
$page = User::paginate(15, 1);Returned structure:
[
'data' => [/* User instances */],
'total' => 120,
'per_page' => 15,
'current_page' => 1,
'last_page' => 8,
]Example in a controller:
$users = User::paginate(15, 1);
return $this->view('users/index', [
'users' => $users['data'],
'pagination' => $users,
]);The framework model includes helpers for understanding whether an instance already exists and whether its attributes have changed.
Checks whether the model currently has a primary key:
if ($user->exists()) {
// Existing persisted record
}Use these to detect changes before saving:
if ($user->isDirty('name')) {
// The name has changed
}
if ($user->isClean()) {
// Nothing has changed
}If you manually change attributes and want to treat the current state as the new baseline:
$user->syncOriginal();Use fresh() when you want a new instance reloaded from the database:
$freshUser = $user->fresh();This is useful after a write when database-level triggers, defaults, or other processes may have changed the stored row.
If your application uses more than one database connection, you can switch the model class to a named connection:
User::useConnection('reporting');Use this deliberately. Since it changes the model's static connection setting, it is best suited to well-bounded application flows.
<?php
declare(strict_types=1);
namespace App\Models;
use Marwa\Framework\Database\Model;
final class User extends Model
{
protected static ?string $table = 'users';
protected static array $fillable = [
'name',
'email',
'active',
'meta',
];
protected static array $casts = [
'active' => 'bool',
'meta' => 'json',
];
}Example usage:
$user = User::updateOrCreate(
['email' => 'alice@example.com'],
[
'name' => 'Alice',
'active' => true,
'meta' => ['role' => 'admin'],
]
);
if ($user->isDirty()) {
$user->saveOrFail();
}
$page = User::paginate(10, 1);- Keep models focused on persistence rules and lightweight domain behavior.
- Use
$fillableconsistently to avoid accidental mass assignment. - Prefer
findBy()andfirstWhere()for straightforward lookups, andnewQuery()when conditions become more complex. - Use
updateOrCreate()for idempotent writes instead of open-coded lookup-then-save sequences. - Use
saveOrFail()anddeleteOrFail()in flows where silent failure would be a bug. - Use the audit hooks when you need a full trail for restore, force delete, or bulk destroy actions.
- Treat
forceFill()anduseConnection()as deliberate tools, not defaults.