ORM (Object-Relational Mapping)
The Osumi Framework includes a lightweight ORM that maps PHP classes to database tables using PHP Attributes. All models must extend the OModel class.
Defining a Model
Models are stored in src/Model/. The framework automatically infers the table name from the class name using snake_case (e.g., ProductCategory class maps to product_category table).
Mandatory Fields
For a model to be valid, it must define the following attributes:
- At least one
#[OPK]: Defines the Primary Key (supports composite keys). - Exactly one
#[OCreatedAt]: Automatically stores the creation timestamp. - Exactly one
#[OUpdatedAt]: Automatically updates on every modification.
Example Model
<?php declare(strict_types=1);
namespace Osumi\OsumiFramework\App\Model;
use Osumi\OsumiFramework\ORM\OModel;
use Osumi\OsumiFramework\ORM\OPK;
use Osumi\OsumiFramework\ORM\OField;
use Osumi\OsumiFramework\ORM\OCreatedAt;
use Osumi\OsumiFramework\ORM\OUpdatedAt;
class User extends OModel {
#[OPK(comment: 'Unique ID')]
public ?int $id = null;
#[OField(max: 100, nullable: false)]
public ?string $name = null;
#[OCreatedAt(comment: 'Creation date')]
public ?string $created_at = null;
#[OUpdatedAt(comment: 'Last update')]
public ?string $updated_at = null;
}
Core Attributes
| Attribute | Description |
|---|---|
#[OPK] |
Primary Key. Can be used on multiple fields for composite keys. Defaults to auto-incremental, otherwise add incr value as false. |
#[OField] |
Standard column. Supports max, nullable, and ref. |
#[OCreatedAt] |
Mandatory field for record creation tracking. |
#[OUpdatedAt] |
Mandatory field for record update tracking. |
**Note on
ref**: Therefparameter (e.g.,ref: 'user.id') is used to define foreign keys in the generated SQL (CREATE TABLE) but does not trigger automatic object loading.
Working with Data
Finding Records
Retreive data using static methods:
where(array $criteria): Returns an array of objects matching the criteria.findOne(array $criteria): Returns a single object ornull.
$user = User::where(['id' => 1]);
if (!is_null($user)) {
echo "User ".$user->name." found";
}
Saving and Updating
The save() method automatically detects if a record is new or existing based on the PK.
$user = new User();
$user->name = 'New User';
$user->save(); // Performs an INSERT
$user = User::findOne(['id' => 1]);
$user->name = 'New name';
$user->save(); // Performs an UPDATE
Validation
The framework calls validate() during the save() process. If a value exceeds max length or is null when not allowed, an Exception is thrown.
Exact DECIMAL Fields
For values that must not pass through floating-point representation, such as monetary amounts, percentages or fixed-precision quantities, use OField::DECIMAL.
#[OField(
type: OField::DECIMAL,
precision: 12,
scale: 2,
nullable: false,
comment: 'Amount'
)]
public ?string $amount = null;
DECIMAL fields are always represented by ?string, never by ?float.
The ORM accepts . and , as decimal separators on input and normalizes the value to . with a fixed scale:
$model->amount = '12,5';
$model->save();
// $model->amount === '12.50'
No automatic rounding is performed. If a value exceeds the configured precision or scale, the ORM throws an error.
Query conditions follow the same rules:
Payment::where([
'amount' => '12,5'
]);
is internally normalized to "12.50", while:
Payment::where([
'amount' => 12.5
]);
is invalid because a float cannot be used for a DECIMAL field.
DECIMAL fields cannot be used as primary keys.
DATE and DATETIME Fields
Since Osumi Framework 9.12, SQL DATE and DATETIME types are represented independently.
An SQL DATE field uses OField::DATE and a PHP ?string property:
#[OField(
type: OField::DATE,
nullable: false
)]
public ?string $event_date = null;
Its canonical format is:
Y-m-d
for example:
2026-10-14
An SQL DATETIME field uses OField::DATETIME:
#[OField(
type: OField::DATETIME,
nullable: false
)]
public ?string $processed_at = null;
Its canonical format is:
Y-m-d H:i:s
for example:
2026-10-14 15:30:45
The ORM validates temporal values strictly. Invalid calendar dates such as 2026-02-30 and invalid times are rejected.
Automatic #[OCreatedAt], #[OUpdatedAt] and #[ODeletedAt] fields internally use DATETIME.
Since Osumi Framework 9.12.1, DATETIME fields can use OField::CURRENT_TIMESTAMP as a dynamic default value:
#[OField(
type: OField::DATETIME,
nullable: false,
default: OField::CURRENT_TIMESTAMP
)]
public ?string $joined_at = null;
When generating the SQL schema, this definition produces:
`joined_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
For new models, the ORM resolves OField::CURRENT_TIMESTAMP to the current timestamp in Y-m-d H:i:s format when default values are applied.
OField::CURRENT_TIMESTAMP can only be used with OField::DATETIME fields. A literal string containing CURRENT_TIMESTAMP remains a normal string value and is not interpreted as an SQL expression.
generateModelFromDB recognizes both CURRENT_TIMESTAMP and CURRENT_TIMESTAMP() on DATETIME columns and generates default: OField::CURRENT_TIMESTAMP. Other dynamic SQL expressions remain unsupported and are explicitly rejected.
The get() method can format both temporal types with an explicit mask:
$model->get(
'event_date',
'd/m/Y'
);
$model->get(
'processed_at',
'd/m/Y H:i'
);
The template date pipe also supports both formats. Without an explicit mask:
DATEusesd/m/Y;DATETIMEusesd/m/Y H:i:s.
BIGINT Fields
Signed SQL BIGINT columns use OField::BIGINT and PHP ?int properties:
#[OField(
type: OField::BIGINT
)]
public ?int $external_id = null;
They may also be used as primary keys:
#[OPK(
type: OField::BIGINT
)]
public ?int $id = null;
BIGINT requires a 64-bit PHP environment.
The ORM does not support BIGINT UNSIGNED columns or ZEROFILL modifiers.
CLI Integration
generateModel: Generates the SQL structure from model definitions, includingDECIMAL(precision,scale),DATE,DATETIMEandBIGINT.generateModelFrom: Generates models from a JSON definition and supportsOField::DECIMAL,OField::DATE,OField::DATETIMEandOField::BIGINT.generateModelFromDB: Interprets SQLDECIMAL(p,s),DATE,DATETIMEandBIGINTcolumns, preserving the types and metadata supported by the ORM.
Best Practices
- Typed Properties: Use public nullable typed properties (e.g.,
public ?string $name = null) for clean initialization. - Default Values: Always initialize properties to
nullto avoid "uninitialized property" errors.