ORM engine includes 2 base classes to carry your database data across application: Record
and RecordEnity
. When Record
class can be described as classic AR implementation, RecordEntity
requires external transaction or UoW to persist it's data.
Implement
RecordInterface
if you want to use custom data carrier.
Record and RecordEntity are cross-compatible.
If you wish to use pure DataMapper implementation take a look at Doctrine, or implement your hydration using
fetchData
method of RecordSelector.
You can read about base entity class here.
To create AR model extent base class Record
.
Your model must include a constant SCHEMA which will describe the table columns and record relationships. Since the ORM component uses DBAL as it's backbone, you can declare a set of desired columns the same way as schema declaration.
Put your model into app/classes/Database/User.php
:
class User extends Record
{
const SCHEMA = [
'id' => 'primary',
'name' => 'string(64)',
'email' => 'string',
'status' => 'enum(active,blocked)'
];
}
ORM requires to have PK for every entity.
You can read how to scaffold your database models and create migrations here.
Declaration of RecordEntity is identical to Record
, though save
method would not exist in your model:
$user = new User();
$user->save();
Versus:
$user = new UserEntity();
$transaction->store($user);
All future descriptions will be given for AR records.
The most important part of any Record/RecordEntity model is it's SCHEMA
. Schema declares what columns you would like to see in the associated table as well as defines a set of record relations. In this tutorial, we will stick to columns only.
Since we are creating a model from scratch (non existing table), we need to declare the table primary key. We can do this the same way as in DBAL Schema Writers by assigning the type "primary" or "bigPrimary".
'id' => 'primary',
Spiral ORM will detect your primary key name automatically so you don't have to use "id".
To provide an additional type arguments, such as length, scale or enum values, you can simply use braces as you would call AbstractColumn
methods.
'name' => 'string(64)',
'status' => 'enum(active,blocked)'
Note that all declared columns are NOT NULL by default, to mark column as nullable use nullable
or null
flag:
'column' => 'string, nullable'
To declare custom default value use constant DEFAULTS
:
const DEFAULES = [
'status' => 'active'
];
Using
null
as default value for column will mark it asnullable
.
By default, Spiral will associate your Record model with the default DBAL database and table whose names are generated based on the class name (in our case, User => users). Y
ou can alter both of these values by declaring constants DATABASE
and TABLE
.
It's recommended to force DATABASE and TABLE constants in all of your entities.
To create indexes in associated table, do this in your Record via constant INDEXES
. Each piece of index information must be located in a sub array and include index type (self::INDEX or self::UNIQUE) and column(s) index it's based on. Let's try to add a unique index to our email
column.
const INDEXES = [
[self::UNIQUE, 'email'],
[self::INDEX, 'email', 'status'] //Compound index
];
To give custom name to an index assing desired value to index type.
const INDEXES = [
[
self::UNIQUE => 'custom_index_name',
'email'
],
];
To persist data associated with your Record or RecordEntity model:
protected function indexAction()
{
$u = new User();
$u->name = 'Anton';
$u->email = 'test@email.com';
$u->status = 'active';
$u->save();
dump($u);
}
Make sure to scaffold database structure and update ORM schema first using
spiral orm:schema -m -vv
.
You can use every DataEntity
method to populate your model such as setFields, setField or even the set of magic methods - setName, setEmail, etc.
Note, method
setFields
will only populate values allowed byFILLABLE
/SECURED
constants.
You can save modified entity calling save
method as you would do that for creation:
protected function updateAction(string $id, UsersRepository $users)
{
if (empty($user = $users->findByPK((int)$id))) {
throw new NotFoundException('No such user');
}
$user->name = 'New Name';
$user->save();
}
For RecordEntity:
$transaction->store($user);
If you check the SQL code generated for the previous example, you might notice that it looks like this:
UPDATE "primary_users"
SET "name" = 'New Name'
WHERE "id" = 1
Record model added only modified fields into update query. This approach is useful in many cases when the data is updated from many places. However, if you want to save your entity data into the database as a solid dataset, you can force the entity "solid state". When the model (Record) is in a solid state every small update will cause a full dataset update.
$user->name = 'New Name';
$user->solidState(true)->save();
This time our SQL will look like this:
UPDATE "primary_users"
SET "name" = 'New Name', "email" = 'test@email.com', "status" = 'active ', "balance" = '0.00'
WHERE "id" = 1
If you want to keep our model in solid state by default, you can call this method in an overwritten model constructor.
To delete AR record and all related data (FK based) call method delete()
, or if you use entity implementation:
$transaction->delete($entity);
Spiral Framework bundle include trait TimestampsTrait
which will automatically add time_created
and time_updated
fields to your model and update them automatically when model is being saved in database:
class User extends Record
{
use TimestampsTrait;
const SCHEMA = [
'id' => 'primary',
'time_registered' => 'datetime',
'name' => 'string(64)',
'email' => 'string',
'status' => 'enum(active,blocked)',
'balance' => 'decimal(10,2)'
];
const INDEXES = [
[self::UNIQUE, 'email']
];
const ACCESSORS = [
'balance' => AtomicNumber::class
];
}
If you wish to access record repository and selector using static functions use Spiral\ORM\SourceTrait
.
class User extends Record
{
use SourceTrait;
const SCHEMA = [
'id' => 'primary',
'name' => 'string',
'email' => 'string'
];
const DEFAULTS = [];
const INDEXES = [];
}
//Repository
dump(User::source());
//Shortcut
dump(User::findOne(['name' => $name]));
Please note, this method will only work in global container scope (inside your application).