A component that gives your app an object-style layer over your SQL database: models map rows to classes, and a fluent query builder handles reads, writes, relations, soft deletes, and pagination. It builds on ForgeDatabaseSQL for the underlying connection.
This is another component your app chooses to use — not a layer you inherit. If you like working with your data as PHP objects and a fluent query interface instead of writing raw SQL, this capability gives you that. You still decide which models exist, how they're shaped, and what your app does with them.
Core capability:
ForgeSqlOrm ships as a core capability
(type: core, loaded after
ForgeDatabaseSQL at order: 1). It
requires a database connection
(DatabaseConnectionInterface) and
provides the
QueryBuilderInterface to your app.
If you prefer raw SQL, or your app only needs a couple of hand-written queries, you can skip this and use the connection from ForgeDatabaseSQL directly. It's your call — these are components.
A core capability, so usually present. If your project needs it, install it like any capability:
php forge.php package:install-module --module=ForgeSqlOrm
It requires ForgeDatabaseSQL to be installed and configured first.
A model is a class that extends the base
Model, declares its table, and maps
columns to typed properties. You add these to your
app wherever you keep your domain code.
use Modules\ForgeSqlOrm\ORM\Model;
use Modules\ForgeSqlOrm\ORM\Attributes\Table;
use Modules\ForgeSqlOrm\ORM\Attributes\Column;
use Modules\ForgeSqlOrm\ORM\Attributes\Hidden;
use Modules\ForgeSqlOrm\ORM\Values\Cast;
#[Table('users')]
class User extends Model
{
#[Column(primary: true)]
public int $id;
#[Column]
public string $email;
#[Column(cast: Cast::JSON)]
public ?array $preferences;
#[Column]
#[Hidden]
public string $password;
}
The base handles the mapping for you: a string
primary key is treated as a UUID (auto-generated
on insert), and created_at /
updated_at are maintained
automatically when those properties exist.
Columns marked #[Hidden] are dropped
from toArray() serialization.
Use the static entry points on your model to build queries:
$user = User::query()->where('email', $email)->first();
$recent = User::query()
->where('active', 1)
->orderBy('created_at', 'DESC')
->limit(10)
->get();
$count = User::query()->count();
$byIds = User::query()->whereIn('id', [1, 2, 3])->get();
There are convenience shortcuts on the model:
$newest = User::latest()->first(); // newest first
$oldest = User::oldest()->first(); // oldest first
New instances save with save(), which
inserts or updates based on whether the row already
exists. Existing rows update only their changed
columns.
$user = new User();
$user->email = 'ada@example.com';
$user->save(); // INSERT
$user->preferences = ['theme' => 'dark'];
$user->save(); // UPDATE (only changed columns)
$user->delete(); // delete, or soft-delete if enabled
You can also operate without loading an instance,
using
insert(), insertMany(),
update(), and forceDelete()
on the query builder.
Mark a column with #[Column(cast: ...)]
to have the ORM convert values in and out of the
database. Supported casts:
INT, FLOAT,
BOOL, STRING,
JSON, DATE,
DATETIME, TIMESTAMP, and
ENUM.
Eager-load relations with with() and
access them on the model. You define how relations
are resolved in your model; this loads them without
N+1 queries:
$posts = Post::with('author', 'comments')->whereIn('id', $ids)->get();
foreach ($posts as $post) {
echo $post->author->name; // already loaded, no extra query
}
Give a model a deleted_at property and
deletes become soft: rows are flagged, not removed.
The
SoftDeletes trait provides the column
for you:
use Modules\ForgeSqlOrm\ORM\Model;
use Modules\ForgeSqlOrm\ORM\Traits\SoftDeletes;
#[Table('posts')]
class Post extends Model
{
use SoftDeletes; // adds $deleted_at column
}
Queries exclude soft-deleted rows by default:
Post::query()->withTrashed()->get(); // include deleted
Post::query()->onlyTrashed()->get(); // only deleted
$post->forceDelete(); // remove for real
For app code that prefers not to call the model
directly, the capability ships a small repository
interface with the usual
create, update,
delete, find,
findBy, findAll, and
query operations.
A QueryCache is available in the
container (default TTL 3600s) with
get, set,
forget, and invalidate
operations so you can cache query results when it
makes sense for your app.
Paginate a model directly and render the links with the included helpers:
$users = User::paginate(
page: 2,
perPage: 20,
column: 'created_at',
direction: 'DESC'
);
// In a view: render the pager and summary
<?= paginate($users) ?>
<?= pagination_info($users) ?>
The pagination(),
paginate(), and
pagination_info() helpers are provided
by this capability.