Model¶
-
class
Model
¶
Probably the most significant class in ATK Data - Model - acts as a Parent for all your entity classes:
class User extends \Atk4\Data\Model
You must define individual classes for all your business entities. Other frameworks may rely on XML or annotations, in ATK everything is defined inside your “Model” class through pure PHP (See Initialization below)
Once you create instance of your model class, it can be recycled. With a single object you can load/unload individual records (See Single record Operations below):
$m = new User($db);
$m = $m->load(3);
$m->set('note', 'just updating');
$m->save();
$m->unload();
$m = $m->load(8);
...
and even perform operations on multiple records (See Persistence Actions below).
When data is loaded from associated Persistence, it is automatically converted into a native PHP type (such as DateTime object) through a process called Typecasting. Various rules apply when you set value for model fields (Normalization) or when data is stored into database that does support a field type (Serialization)
Furthermore, because you define Models as a class, it is very easy to introduce your own extensions which may include Hooks and Actions.
There are many advanced topics that ATK Data covers, such as References, Joins, Aggregation, SQL actions, Unions, Deep Traversal and Containment.
The design is also very extensible allowing you to introduce new Field types, Join strategies, Reference patterns, Action types.
I suggest you to read the next section to make sure you fully understand the Model and its role in ATK Data.
Understanding Model¶
Please understand that Model in ATK Data is unlike models in other data frameworks. The Model class can be seen as a “gateway” between your code and many other features of ATK Data.
For example - you may define fields and relations for the model:
$model->addField('age', ['type' => 'integer']);
$model->hasMany('Children', ['model' => [Person::class]]);
Methods addField and hasMany will ultimatelly create and link model with a corresponding Field object and Reference object. Those classes contain the logic, but in 95% of the use-cases, you will not have to dive deep into them.
Model object = Data Set¶
From the moment when you create instance of your model class, it represents a DataSet - set of records that share some common traits:
$allUsers = new User($db); // John, Susan, Leo, Bill, Karen
Certain operations may “shrink” this set, such as adding conditions:
$maleUsers = $allUsers->addCondition('gender', 'M');
send_email_to_users($maleUsers);
This essentially filters your users without fetching them from the database server. In my example, when I pass $maleUsers to the method, no records are loaded yet from the database. It is up to the implementation of send_email_to_users to load or iterate records or perhaps approach the data-set differently, e.g. execute multi-record operation.
Note that most operations on Model are mutating (meaning that in example above $allUsers will also be filtered and in fact, $allUsers and $maleUsers will reference same object. Use clone if you do not wish to affect $allUsers.
Model object = meta information¶
By design, Model object does not have direct knowledge of higher level objects or specific implementations. Still - Model will be a good place to deposit some meta-information:
$model->addField('age', ['ui' => ['caption' => 'Put your age here']]);
Model and Field class will simply store the “ui” property which may (or may not) be used by ATK UI component or some add-on.
Domain vs Persistence¶
When you declare a model Field you can also store some persistence-related meta-information:
// override how your persistence formats date field
$model->addField('date_of_birth', ['type' => 'date', 'persistence' => ['format' => 'Ymd']]);
// declare field which is not saved
$model->addField('secret', ['neverPersist' => true]);
// rellocate into a different field
$model->addField('old_field', ['actual' => 'new_field']);
// or even into a different table
$model->join('new_table')->addField('extra_field');
Model also has a property $table, which indicate name of default table/collection/file to be used by persistence. (Name of property is decided to avoid beginner confusion)
Good naming for a Model¶
Some parts of this documentation were created years ago and may use class notation: Model_User. We actually recommend you to use namespaces instead:
namespace yourapp\Model;
use \Atk4\Data\Model;
class User extends Model
{
protected function init(): void
{
parent::init();
$this->addField('name');
$this->hasMany('Invoices', ['model' => [Invoice::class]]);
}
}
PHP does not have a “class” type, so Invoice::class will translate into a string “yourappModelInvoice” and is a most efficient way to specify related class name.
You way also use new Invoice() there but be sure not to specify any argument, unless you intend to use cross-persistence referencing (this is further explained in Advanced section)
Initialization¶
-
Model::
init
()¶
Method init() will automatically be called when your Model is associated with Persistence object. It is commonly used to declare fields, conditions, relations, hooks and more:
class Model_User extends Atk4\Data\Model
{
protected function init(): void
{
parent::init();
$this->addField('name');
$this->addField('surname');
}
}
You may safely rely on $this->getPersistence() result to make choices:
if ($this->getPersistence() instanceof \Atk4\Data\Persistence\Sql) {
// Calculating on SQL server is more efficient!!
$this->addExpression('total', ['expr' => '[amount] + [vat]']);
} else {
// Fallback
$this->addCalculatedField('total', ['expr' => function (self $m) {
return $m->get('amount') + $m->get('vat');
}, 'type' => 'float']);
}
To invoke code from init() methods of ALL models (for example soft-delete logic), you use Persistence’s “afterAdd” hook. This will not affect ALL models but just models which are associated with said persistence:
$db->onHook(Persistence::HOOK_AFTER_ADD, function (Persistence $p, Model $m) use ($acl) {
$fields = $m->getFields();
$acl->disableRestrictedFields($fields);
});
$invoice = new Invoice($db);
Fields¶
Each model field is represented by a Field object:
$model->addField('name');
var_dump($model->getField('name'));
Other persistence framework will use “properties”, because individual objects may impact performance. In ATK Data this is not an issue, because “Model” is re-usable:
foreach (new User($db) as $user) {
// will be the same object every time!!
var_dump($user->getField['name']);
// this is also the same object every time!!
var_dump($user);
}
Instead, Field handles many very valuable operations which would otherwise fall on the
shoulders of developer (Read more here Field
)
-
Model::
addField
($name, $seed)¶
Creates a new field object inside your model (by default the class is ‘Field’). The fields are implemented on top of Containers from Agile Core.
Second argument to addField() will contain a seed for the Field class:
$this->addField('surname', ['default' => 'Smith']);
You may also specify your own Field implementation:
$this->addField('amount_and_currency', [MyAmountCurrencyField::class]);
Read more about Field
-
Model::
addFields
(array $fields, $seed = [])¶
Creates multiple field objects in one method call. See multiple syntax examples:
$m->addFields(['name'], ['default' => 'anonymous']);
$m->addFields([
'last_name',
'login' => ['default' => 'unknown'],
'salary' => ['type' => 'atk4_money', CustomField::class, 'default' => 100],
['tax', CustomField::class, 'type' => 'atk4_money', 'default' => 20],
'vat' => new CustomField(['type' => 'atk4_money', 'default' => 15]),
]);
Read-only Fields¶
Although you may make any field read-only:
$this->addField('name', ['readOnly' => true]);
There are two methods for adding dynamically calculated fields.
-
Model::
addExpression
($name, $seed)¶
Defines a field as server-side expression (e.g. SQL):
$this->addExpression('total', ['expr' => '[amount] + [vat]']);
The above code is executed on the server (SQL) and can be very powerful. You must make sure that expression is valid for current $this->getPersistence():
$product->addExpression('discount', ['expr' => $this->refLink('category_id')->fieldQuery('default_discount')]);
// expression as a sub-select from referenced model (Category) imported as a read-only field
// of $product model
$product->addExpression('total', ['expr' => 'if ([is_discounted], ([amount] + [vat])*[discount], [amount] + [vat])']);
// new "total" field now contains complex logic, which is executed in SQL
$product->addCondition('total', '<', 10);
// filter products that cost less than 10.0 (including discount)
For the times when you are not working with SQL persistence, you can calculate field in PHP.
-
Model::
addCalculatedField
($name[, 'expr' => $callback])¶
Creates new field object inside your model. Field value will be automatically calculated by your callback method right after individual record is loaded by the model:
$this->addField('term', ['caption' => 'Repayment term in months', 'default' => 36]);
$this->addField('rate', ['caption' => 'APR %', 'default' => 5]);
$this->addCalculatedField('interest', ['expr' => function (self $m) {
return $m->calculateInterest();
}, 'type' => 'float']);
Important
always use argument $m instead of $this inside your callbacks. If model is to be cloned, the code relying on $this would reference original model, but the code using $m will properly address the model which triggered the callback.
This can also be useful for calculating relative times:
class MyModel extends Model
{
use HumanTiming; // see https://stackoverflow.com/questions/2915864/php-how-to-find-the-time-elapsed-since-a-date-time
protected function init(): void
{
parent::init();
$this->addCalculatedField('event_ts_human_friendly', ['expr' => function (self $m) {
return $this->humanTiming($m->get('event_ts'));
}]);
}
}
Actions¶
Another common thing to define inside Model::init()
would be
a user invokable actions:
class User extends Model
{
protected function init(): void
{
parent::init();
$this->addField('name');
$this->addField('email');
$this->addField('password');
$this->addUserAction('send_new_password');
}
public function send_new_password()
{
// .. code here
$this->save(['password' => .. ]);
return 'generated and sent password to ' . $m->get('name');
}
}
With a method alone, you can generate and send passwords:
$user = $user->load(3);
$user->send_new_password();
but using $this->addUserAction() exposes that method to the ATK UI wigets, so if your admin is using Crud, a new button will be available allowing passwords to be generated and sent to the users:
Crud::addTo($app)->setModel(new User($app->db));
Read more about ModelUserAction
Hooks¶
Hooks (behaviours) can allow you to define callbacks which would trigger
when data is loaded, saved, deleted etc. Hooks are typically defined in
Model::init()
but will be executed accordingly.
There are countless uses for hooks and even more opportunities to use hook by all sorts of extensions.
Validation¶
Validation is an extensive topic, but the simplest use-case would be through a hook:
$this->addField('name');
$this->onHookShort(Model::HOOK_VALIDATE, function () {
if ($this->get('name') === 'C#') {
return ['name' => 'No sharp objects are allowed'];
}
});
Now if you attempt to save object, you will receive ValidationException
:
$model->set('name', 'Swift');
$model->saveAndUnload(); // all good
$model->set('name', 'C#');
$model->saveAndUnload(); // exception here
Inheritance¶
ATK Data models are really good for structuring hierarchically. Here is example:
class VipUser extends User
{
protected function init(): void
{
parent::init();
$this->addCondition('purchases', '>', 1000);
$this->addUserAction('send_gift');
}
public function send_gift()
{
...
}
}
This introduces a new business object, which is a sub-set of User. The new class will inherit all the fields, methods and actions of “User” class but will introduce one new action - send_gift.
Associating Model with Database¶
After talking extensively about model definition, lets discuss how model is associated with persistence. In the most basic form, model is associated with persistence like this:
$m = new User($db);
If model was created without persistence Model::init()
will not fire. You can
explicitly associate model with persistence like this:
$m = new User();
// ....
$m->setPersistence($db); // links with persistence
Multiple models can be associated with the same persistence. Here are also some examples of static persistence:
$m = new Model(new Persistence\Static_(['john', 'peter', 'steve']);
$m = $m->load(1);
echo $m->get('name'); // peter
See Persistence\Static_
-
property
Model::$
persistence
¶
Refers to the persistence driver in use by current model. Calling certain methods such as save(), addCondition() or action() will rely on this property.
-
property
Model::$
persistenceData
¶
DO NOT USE: Array containing arbitrary data by a specific persistence layer.
-
property
Model::$
table
¶
If $table property is set, then your persistence driver will use it as default table / collection when loading data. If you omit the table, you should specify it when associating model with database:
$m = new User($db, 'user');
This also overrides current table value.
-
Model::
withPersistence
($persistence)¶
Creates a duplicate of a current model and associate new copy with a specified persistence. This method is useful for moving model data from one persistence to another.
Populating Data¶
-
Model::
insert
($row)¶ Inserts a new record into the database and returns $id. It does not affect currently loaded record and in practice would be similar to:
$entity = $m->createEntity(); $entity->setMulti($row); $entity->save(); return $entity;
The main goal for insert() method is to be as fast as possible, while still performing data validation. After inserting method will return cloned model.
-
Model::
import
($data)¶ Similar to insert() however works across array of rows. This method will not return any IDs or models and is optimized for importing large amounts of data.
The method will still convert the data needed and operate with joined tables as needed. If you wish to access tables directly, you’ll have to look into Persistence::insert($m, $data);
Working with selective fields¶
When you normally work with your model then all fields are available and will be loaded / saved. You may, however, specify that you wish to load only a sub-set of fields.
-
Model::
setOnlyFields
($fields)¶ Specify array of fields. Only those fields will be accessible and will be loaded / saved. Attempt to access any other field will result in exception.
Null restore to full set of fields. This will also unload active record.
-
property
Model::$
onlyFields
¶ Contains list of fields to be loaded / accessed.
Setting and Getting active record data¶
When your record is loaded from database, record data is stored inside the $data property:
-
property
Model::$
data
¶ Contains the data for an active record.
Model allows you to work with the data of single a record directly. You should use the following syntax when accessing fields of an active record:
$m->set('name', 'John');
$m->set('surname', 'Peter');
// or
$m->setMulti(['name' => 'John', 'surname' => 'Peter']);
When you modify active record, it keeps the original value in the $dirty array:
-
Model::
set
($field, $value)¶ Set field to a specified value. The original value will be stored in $dirty property.
-
Model::
setMulti
($fields)¶ Set multiple field values.
-
Model::
setNull
($field)¶ Set value of a specified field to NULL, temporarily ignoring normalization routine. Only use this if you intend to set a correct value shortly after.
-
Model::
unset
($field)¶ Restore field value to it’s original:
$m->set('name', 'John'); echo $m->get('name'); // John $m->_unset('name'); echo $m->get('name'); // Original value is shown
This will restore original value of the field.
-
Model::
get
()¶ Returns one of the following:
- If value was set() to the field, this value is returned
- If field was loaded from database, return original value
- if field had default set, returns default
- returns null.
-
Model::
isset
()¶ Return true if field contains unsaved changes (dirty):
$m->_isset('name'); // returns false $m->set('name', 'Other Name'); $m->_isset('name'); // returns true
-
Model::
isDirty
()¶ Return true if one or multiple fields contain unsaved changes (dirty):
if ($m->isDirty(['name', 'surname'])) { $m->set('full_name', $m->get('name') . ' ' . $m->get('surname')); }
When the code above is placed in beforeSave hook, it will only be executed when certain fields have been changed. If your recalculations are expensive, it’s pretty handy to rely on “dirty” fields to avoid some complex logic.
-
property
Model::$
dirty
¶ Contains list of modified fields since last loading and their original values.
-
Model::
hasField
($field)¶ Returns true if a field with a corresponding name exists.
-
Model::
getField
($field)¶ Finds a field with a corresponding name. Throws exception if field not found.
Full example:
$m = new Model_User($db, 'user');
// Fields can be added after model is created
$m->addField('salary', ['default' => 1000]);
echo $m->_isset('salary'); // false
echo $m->get('salary'); // 1000
// Next we load record from $db
$m = $m->load(1);
echo $m->get('salary'); // 2000 (from db)
echo $m->_isset('salary'); // false, was not changed
$m->set('salary', 3000);
echo $m->get('salary'); // 3000 (changed)
echo $m->_isset('salary'); // true
$m->_unset('salary'); // return to original value
echo $m->get('salary'); // 2000
echo $m->_isset('salary'); // false
$m->set('salary', 3000);
$m->save();
echo $m->get('salary'); // 3000 (now in db)
echo $m->_isset('salary'); // false
-
protected
Model::
normalizeFieldName
()¶ Verify and convert first argument got get / set;
Setting limit and sort order¶
-
public
Model::
setLimit
($count, $offset = null)¶ Sets limit on how many records to select. Will select only $count records starting from $offset record.
-
public
Model::
setOrder
($field, $desc = null)¶ Sets sorting order of returned data records. Here are some usage examples. All these syntaxes work the same:
$m->setOrder('name, salary desc'); $m->setOrder(['name', 'salary desc']); $m->setOrder(['name', 'salary' => true]); $m->setOrder(['name' => false, 'salary' => true]); $m->setOrder([ ['name'], ['salary', 'desc'] ]); $m->setOrder([ ['name'], ['salary', true] ]); $m->setOrder([ ['name'], ['salary desc'] ]); // and there can be many more similar combinations how to call this
Keep in mind - true means desc, desc means descending. Otherwise it will be ascending order by default.
You can also use Atk4DataPersistenceSqlExpression or array of expressions instead of field name here. Or even mix them together:
$m->setOrder($m->expr('[net] * [vat]')); $m->setOrder([$m->expr('[net] * [vat]'), $m->expr('[closing] - [opening]')]); $m->setOrder(['net', $m->expr('[net] * [vat]', 'ref_no')]);