Data modeling - using models

This tutorial unleashes the model usage.

After reading this tutorial the Model API can be used as a reference to see all available properties and methods.

Adding data

To add new data set the new values to the model data properties, then call the add() method. This adds an action to model action queue.

Data is inserted into the database only after calling commit().

Each model intance has it's own action queue. Commiting one instance actions will not effect the other model instances. But it is possible to commit all queues in one transaction using ModelX class.

/* Add new data */
$user = new User();
$user->name = 'Arta';
$user->active = 'Y';
$user->Group->id = 4;
$ok = $user->add()->commit();

/* Same as above ->__key__ gives model key which is id */
$user = new User();
$user->name = 'Arta';
$user->active = 'Y';
$user->Group->{$user->Group->__key__} = 4;
$ok = $user->add()->commit();

/* Get group id by knowing Group.name */
$user = new User();
$user->name = 'Arta';
$user->active = 'Y';
$user->Group->name = 'Engines';
$user->Group->query()->next();
$ok = $user->add()->commit();

Add more rows of data:

$p = new Pets();

$p->name = 'Bunny';
$p->Type->id = 1;
$p->add();

$p->name = 'Rabbit';
$p->add();

$p->name = 'Kitty';
$p->Type->id = 2;
$p->add();

$p->name = 'Catty';
$p->add();

$p->name = 'Puppy';
$p->Type->id = 3;
$p->add();

$p->name = 'Lyra';
$p->add();

/* commit them all */
if ($user->commit()) {
    echo 'Successfully';
}

/* to get key value of new data */
echo $user->keyVal();

/* to cancel instead of commit */
$user->cancel();

Add object first:

$user = new User();
$user->name = 'Arta';

$group = $user->Group;
$group->name = 'Engines';

if ($group->add()->commit()) {
    /* changing $group changes $user->Group */
    $group->id = $group->keyVal();
    $ok = $user->add()->commit();
}

Add model object list:

$group = new Group;
$group->name = 'End user';
/* add 3 permission objects */
$permission1 = new Permission();
$permission1->id = 4;
$group->Permission[] = $permission1;

$permission2 = new Permission();
$permission2->id = 32;
$group->Permission[] = $permission2;

$permission3 = new Permission();
$permission3->id = 67;
$group->Permission[] = $permission2;

$group->add()->commit();

Events

You can define two event methods related to adding data in a model, if any exist it will be called when the event happens.

  • Will be called before add() starts to work. The return value of this event is important, returning False will ignore adding.
  • Will be called after adding and committing. The first argument is a boolean giving the state of success. The second argument is an array of {property: value,}. Because if this event happens after a queue/transaction commit and there can be more than one actions in a queue, and model data can change after each action. To have the exact model data at the moment of the specific action this argument can be used.
class User {

    /* properties and constructor will be here ... */

    public function beforeAdd() {
        /* if name is set to Lyra dont add and commit data*/
        if ($this->name === 'Lyra') {
            return false
        }
        /* you can check validate and manipulate model data
           before bein add */
        $this->height = (int)$this->height * 1000;
        return true;
    }

    public function afterAdd($ok, $modelData) {
        if ($ok) {
            Log::model('logObjName', $this);
        } else {
            Log::warning('logObjName', 'User not added');
        }
    }
}

Updating data

Use update() to add "data update" action to action queue then commit actions by commit().

If model key property has value then data for the row with that key will be updated:

$user = new User();
$user->id = 1;
$user->name = 'updated name';
$ok = $user->update()->commit();

Update by conditioned:

$user = new User();
$user->active = 'Y';
/* 
  update rows which active = N and Group->id = 5
  operator is 'AND' unless $this->operator = 'OR'; 
 */
$user->update(
    array(
        'active' => 'N',
        'Group'  => 5,
    )
);
/*
  update rows which active = N and Group->id = 6
 */
$user->update(
    'active = $active AND UserGroup = $group',
    array('active' => 'N', 'group' => 7)
);
/*
  update rows which active = N and Group->id = 8
 */
$user->update('active = $1 AND Group = $2', 'N', 8);
/* commit them all set active = Y */
$user->commit();

There is no hint so all rows will be updated:

$user = new User();
$user->active = 'Y';
$user->update()->commit();

Important note

If a data property value is Null it will be ignored by update! To update a property value to Null. Set value to Model::N:

$user->description = Model::N;

Events

You can define this methods in a model, if they exist they will be called when the event happens.

  • Will be called before "update()" starts to work. The return value of this event is important, returning False will ignore updating.
  • Will be called after updating and committing. The first argument is a boolean giving the state of success. The second argument is an array of {property: value,}. Because if this event happens after a queue/transaction commit and there can be more than one actions in a queue, and model data can change after each action. To have the exact model data at the moment of the specific action this argument can be used.

Deleting data

Use delete() to add "delete action" to action queue then commit actions by commit().

If model key property has value then data for the row with that key will be updated:

$user = new User();
$user->id = 1;
$$user->delete()->commit();

Delete by condition:

$user = new User();
/* 
  delete rows which active = N and Group->id = 5
  operator is 'AND' unless $this->operator = 'OR'; 
 */
$user->delete(
    array(
        'active' => 'N',
        'Group'  => 5,
    )
);
/*
  delete rows which active = N and Group->id = 6
 */
$user->delete(
    'active = $active AND UserGroup = $group',
    array('active' => 'N', 'group' => 7)
);
/*
  delete rows which active = N and Group->id = 8
 */
$user->delete('active = $1 AND Group = $2', 'N', 8);
/* commit them all */
$user->commit();

All rows will be deleted:

$user = new User();
$user->delete()->commit();

If in a model public $__delete__ = False; then delete() will not physically remove data from database but will mark data as deleted. Marked as deleted data are ignored by the model.

Events

You can define this methods in a model, if they exist they will be called when the event happens.

  • Will be called before "delete()" starts to work. The return value of this event is important, returning False will ignore deleting.
  • Will be called if last action was delete after calling "commit()". The first argument is a boolean giving the state of success. The second argument is an array of {property: value,}. Because if this event happens after a queue/transaction commit and there can be more than one actions in a queue, and model data can change after each action. To have the exact model data at the moment of the specific action this argument can be used.
    public function beforeDelete() {
        // stop deleting
        return false;
        // delete
        return true;
        // update this properties when marking as deleted
        return array('acive' => 'N', 'status' => 'bad');
    }

    public function afterDelete($ok, $modelData) {
        if ($ok) {
            Log::model('logObjName', $this);
        } else {
            Log::warning('logObjName', 'User not removed');
        }
    }

When marking as deleted, it is possible to update other property values. This is cruicial when model has one or more unique property(s). Because the row is not really deleted and adding data with the same values for unique properties will cause errors.

    public function beforeDelete() {
        /* the value of username and email will be updated
           to something safe */
        return array(
            // putting ! before column name will cause value not to be escaped
            '!username' => $this->id.'!'.$this->username,
            '!email'    => $this->id.'!'.$this->email,
        );
    }

Query data

Method query() queries model data however there are more things to know around query such as how the query method can be tweaked at database level, config conditions, sorting, paging, limiting and so on. This subjects will be covered in this section.

Query, sort and limit

When building the application Artaengine creates three or four database views for each model, this views are used by the query() query database. Custom views can be created by the programmer and used over the default ones.

Views are named as: v_modelname_postfix the postfix part is optional when naming custom views. Below is a description on the default views created by Artaengine:

  • $model->query(); will use this view. This view covers all relations of model table with other tables and so on. Querying using this view, data can be accessed via model object at any stage. For example $modelObj->Client->Company->City->Country->name; will give the country name of the client company if there is a link of model objects as above.
  • $model->query(Model::QUERY_SELF); use this view. Using this view, only model self scalar data can be queried, model object data will be Null. For example $modelObj->name; is usable but $modelObj->Client->name and further objects such as $modelObj->Client->Company->City->Country->name; will not be usable.
  • $model->query(Model::QUERY_L1); Using this view, only model self scalar data and first stage objects can be queried. For example $modelObj->name; and $modelObj->Client->name are usable but anything further such as $modelObj->Client->Company->City->Country->name; will not be usable.

Choosing a good view is based on the required data and how deep model objects go.

Examples:

$user = new User();
/* specify view */
$user->query();
$user->query(Model::QUERY_SELF);
$user->query(Model::QUERY_L1);
$user->query('_custom');

/* get count of queried rows */
$count = $user->count();

/* sort by Group and id */
$user->sort('Group ASC, id DESC')->query();

/* limit query to 30 rows starting from row 100 (first row = 0) */
$user->sort('id DESC')->limit(30, 100)->query();

/* limit query to rows between 100 and 180 (first row = 1) */
$user->sort('id DESC')->between(100, 180)->query();

/* limit query to the top 70 rows */
$user->sort('id DESC')->top(70)->query();

Note: Any method effecting queries such as sort, limit and the methods which will be explained later in this section which can be used in object chains can come in any order before method query itself.

Slicing results into pages

Method slice($configs) will cause method query to slice query result into pages based on $configs then creates and employs a Paging object which can be used to render paging links if needed.

See Paging API for all possible configs .

See Paging tutorial to learn about paging .

/* get page 2 where each page contains 30 rows of data */
$settings = array(
    'link' => Paging::LINK_JS, // type of links for rendering paging links
    'page' => 2,
    //'max_rows' => 40, default is 30
);
/* query model data for page 2 */
$user->slice($settings)
     ->sort('createtime DESC')
     ->query();
/* get paging links and show them on browser */
$pageingLinksHTML = $user->__page__->render();
/* 
 $user->__page__ is an object of class Paging
 */

Conditioning

If model data properties are set to values they will affect querying conditions:

$user = new User();
$user->id = 5;
$user->active = 'Y';
$user->Group = 3;
/* id = 5 AND active = 'Y' AND Group.id = 3 */
$user->query();

$user->operator = 'OR';
/* id = 5 OR active = 'Y' OR Group.id = 3 */
$user->query();

Using method where:

/* To condition model object properties use ModelClassName.property: */
$photo = new Photo();

$user->where("Archive.name = '2011-02-07 Rome' OR Archive.id = 1")
     ->query();

$user->where("Archive.id IN (1, 7, 21)")
     ->query();

/* To condition model object list properties only use
   ModelClassName.keyProperty IN (key-values) */
$user->where("Label.id IN (12, 18, 211, 54)")
     ->query();

$user = new User();

$user->where("Group.id = 1 AND active = 'Y'")->query();

$user->where('Group.id = $1 AND active = $2', 1, 'Y')->query();

$user
    ->where(
        'Group.id = $group AND active = $active',
        array('group' => 1, 'active' => 'Y')
    )
    ->query();

Using method where and pure database SQL statements:

$this->__pureSql__ = True;
/* make conditions as you would on database */
$user->where('group_id = $1 AND active = $2', 1, 'Y')->query();

When using where while model properties are set to values:

$user = new User();
$user->id = 5;
$user->active = 'Y';
$user->where("Group.id = 1")->query();
/* Queries: id = 5 AND active = 'Y' AND Group.id = 1 */

Each method where clears previous method where conditions. To add conditions without clearing previous conditions use method extendWhere.

$user = new User();
$user->where("id = 1")
     ->where("name = 'Arta'")
     ->where("active = 'Y'")
     ->where("Group.id = 3")
     ->query();
// < Group.id = 2

$user = new User();
$user->where("id = 1")
     ->extendWhere("name = 'Arta'")
     ->extendWhere("active = 'Y'")
     ->extendWhere("Group.id = 3")
     ->query();
// < id = 1 AND name = 'Arta' AND active = 'Y' AND Group.id = 2

$user = new User();
$user->extendWhere("id = 1");
$user->extendWhere("name = 'Arta'");
$user->extendWhere("active = 'Y'");
$user->extendWhere("Group.id = 3")->query();
// < id = 1 AND name = 'Arta' AND active = 'Y' AND Group.id = 2

Fetching data

Fetch one row:

$user = new User();
$user->query();
if ($user->next()) {
    echo 'id = '.$user->id.' name = '. $user->name;
} else {
    echo 'No data';
}

Fetch 5th row:

if ($user->next(5)) {
    echo 'id = '.$user->id.' name = '. $user->name;
} else {
    echo 'No data';
}

Method next and while:

$user = new User();
$user->query();
while ($user->next()) {
    echo $user->name.'<br/>';
}

Iterate on model object:

$user = new User();
$user->query();
foreach ($user as $rowNumber => $row) {
    echo $user->name.'<br/>';
}

Iterating on model object lets doing things lie getting data from model methods which format property data.

Model objects can be used in templates for presenting data.

Events

If the methods shown below exists in a model class they will be called on events based on querying and fetching data:

  • Will be called before method query acts. The return value of this event is important, returning False will stop method query running.
  • Will be called after query id done.
  • Will be called after fetching each row of data. This event can be used to format or alter model data after being fetched.
class users {
    /* properties and constructor will be here ... */

    public function beforeQuery() {
        /* things like checking the conditions or adding extra conditions */
        return True;
    }

    public function afterQuery() {
        /* what to be done afte query */
    }

    public function afterNext() {
        /* imagine class Persian::g2j coverts georgian date to jalali date */
        $this->createdate = Persian::g2j($this->createdate);
    }
}

Midware

Events on add, update, delete, query and fetch actions beside using methods for formatting and altering data can be very useful and powerful to encapsulate models as data entities and maintain the integrity by keeping all actions related to the data entity inside the model class.

However using midwares with models is another alternative. Sometimes the same thing is done on several models data, for example if data is stored in different metrics but needs to be presented in another system. In this situation a class called midware can be created which will have some methods which will be used by models in situations.

A midware can be related to scalar data properties. Below is a example midware class:

// example midware class
// Class name and PHP name myst be the same: Persian.php
// a mid-ware class must be visible to autoload

class Persian {
    /* called after Model.next()
       only non object properties */
    static function afterNext($currentModelPropertyVal) {
        /* some processing */
        return $valToBeReplacedWithCurrentPropertyVal;
    }
    /* called after Model.add() and Model.update()
       only non object properties */
    static function beforeStore($currentModelPropertyVal) {
        /* some processing */
        return $valToBeStoredInsteadOfCurrentVal;
    }
    /* called before Model.query() and FormSearch.bind()
       only non object properties */
    static function beforeQuery($currentModelPropertyVal) {
        /* some processing */
        return $valToBeUsedForConditionInsteadOfCurrentVal;
    }
}

Below is a model which is using above midware:

class Content {

    public $id = array(
        'key'      => True,
        'label'    => 'کد',
    );

    public $title = array(
        'type'     => STRING,
        'len'      => 50,
    );

    public $content = array(
        'type'     => WYSIWYG,
        'midware'  => 'Persian',
    );

    public $create_dt = array(
        'type'     => TIMESTAMP,
        'required' => True,
        'default'  => 'NOW()',
        'midware'  => 'Persian',
    );
}

In the above example midware is defined on "title" and "create_dt" properties. When events defined inside the midware happen property data of "title" and "create_dt" are sent to midware methods and will be replaces with what midware methods return.

Binding data

Data can be pulled from model data properties into an array or the opposite can be done, array data can be pushed into model data properties. Class Binder lets pulling and pushing model data.

Pushing array data into a model:

$data = array(
    'id'        => 1,
    'UserGroup' => 4,  // object key value
    'City'      => 77, // object key value
    'name'      => 'Myname',
    'family'    => 'Myfamily',
    'active'    => 'Y',
    'email'     => 'myemail@myemail.com',
    'username'  => 'myusername',
);
$user = new User();
Binder::push($user, $data);

Pushing array data into a model - when source is a PHP request array:

Binder::push($user, INPUT_GET);
Binder::push($user, 'get');

Binder::push($user, INPUT_POST);
Binder::push($user, 'post');

Binder::push($user, INPUT_COOKIE);
Binder::push($user, 'cookie');

Binder::push($user, INPUT_SESSION);
Binder::push($user, 'session');

Binder::push($user, INPUT_REQUEST);
Binder::push($user, 'request');

Pushing array data into a model - configs:

$data = array(
    'User-id-X'        => 1,
    'User-UserGroup-X' => 4,  // object key value
    'User-City-X'      => 77, // object key value
    'User-name-X'      => 'Myname',
    'User-family-X'    => 'Myfamily',
    'User-active-X'    => 'Y',
    'User-email-X'     => 'myemail@myemail.com',
    'User-username-X'  => 'myusername',
);
$user = new User();
Binder::push(
    $user,
    $data,
    array(
         // to match array key names with model property names
        'prefix'  => 'User-',
        'postfix' => '-X',
    )
);

By default if a property not exists in array or the array value is Null then the property value will remain what it was and not set to Null or anything else.

Add 'default' = True; to the configs so when property not exists in array or array value is Null then property value will be set to Binder::DEFAULT_VALUE = Null.

To change default value to something else set 'default_value' key to something else in the configs.

Bind::push accepts objects as well as arrays.

Pull data from model to array:

$user = new User();

$array = Binder:pull:($user);

$array = Binder:pull:(
    $user, 
    array(
         // to add prefix of/and postfix to array keys
        'prefix'  => 'User-',
        'postfix' => '-X',
    )
);

Data validation

Data properties of a model can be validated by Validate class:

$user = new User();
$user->age = 'bad value';
$user->email = 'bad value';
$user->Group->id = 'bad value';

$errorMsg = array(
    Validate::REQUIRED          => 'required',
    Validate::NUMBER_SMALL      => 'number is too small',
    Validate::NUMBER_LARGE      => 'number is too large',
    Validate::INVALID_STRING    => 'invalid string',
    Validate::INVALID_INT       => 'invalid integer',
    Validate::INVALID_BOOL      => 'invalid boolean',
    Validate::INVALID_FLOAT     => 'invalid float',
    Validate::INVALID_ARRAY     => 'invalid array',
    Validate::INVALID_DATE      => 'invalid date',
    Validate::INVALID_TIME      => 'invalid time',
    Validate::INVALID_DATETIME  => 'invalid date time',
    Validate::STRING_SHORT      => 'string is too short',
    Validate::STRING_LONG       => 'string is too long',
    Validate::FORMAT_IP         => 'invalid IP',
    Validate::FORMAT_URL        => 'invalid URL',
    Validate::FORMAT_EMAIL      => 'invalid email',
);

/* validate all model properties */
if ($errors=Validate::evaluate($user)) {
    foreach($errors as $property => $propertyErrors) {
        foreach($propertyErrors as $error) {
            echo $property.' - '.$errorMsg[$error].'<br/>';
        }
    }
}
/* validate model scalar data ptoperty */
if ($errors=Validate::evaluateProperty($user->name, $user->cols('name'))) {
    echo 'Name - '.$errorMsg[$error];
}

Validate::evaluate() returns an empty array if model is filled with good data or a dictionary of property names and a list of errors found for each {property-name: [error,],}. Errors are constants.

Class Force will try to fix bad data or Null them if not fixable. Using Force is not a recommended way to treat data.

Force::evaluate($user);

Class ModelX

To commit actions of a group of models at once or in one transaction:

$group = new Group();
$group->id = 5;
$group->name = 'Admin';
$group->update();

$user = new User();
$user->name = 'Arta';
$user->email = 'arta@artaengine.com';
$user->Group->id = 5;
$user->add();

$photo = new Photo();
$photo->id = 31;
$group->delete();
$photo->id = 155;
$group->delete();
$photo->id = 451;
$group->delete();

/* to commit all actions in one transaction: */
ModelX::commit('modelGroupName');

/* to commit all actions at once but not as transaction: */
ModelX::commit('modelGroupName', False);

/* to cancel all actions: */
ModelX::cancel('modelGroupName');

To inspect last SQLs and transactions executed on database by models:

/* by model group name */
ModelX::inspect('modelGroupName');
/* by model object */
ModelX::inspect($user);

Class Xtract

To extract key values after a query:

$user = new User();
$user->query();
$keys = Xtract::key($user);
/* $keys would be a list of queried key values:
   array(1, 2, 4, 11) */

To extract property values after a query:

$user = new User();
$user->query();
$emails = Xtract::col($user, 'email');
/* $emails would be a list of queried values for property email:
   array(
      'e1@example.com',
      'e2@example.com',
      'e4@example.com',
      'e11@example.com'
   ) */

To extract all values by key after a query:

$user = new User();
$user->query();

$emailsByKey = Xtract::byKey($user, 'email');
/* $emailsByKey would be a list of queried values for property email by key:
   array(
      1  => 'e1@example.com',
      2  => 'e2@example.com',
      4  => 'e4@example.com',
      11 => 'e11@example.com'
   ) */

$allByKey = Xtract::byKey($user);
/* $allByKey would be a list of queried values by key:
   array(
      1  => array('email' => 'e1@example.com', 'name' => '1', ),
      2  => array('email' => 'e2@example.com', 'name' => '2', ),
      4  => array('email' => 'e4@example.com', 'name' => '4', ),
      11 => array('email' => 'e11@example.com', 'name' => '5', ),
   ) */

Class MakeReadable

Class MakeReadable alters the queried model data which are not suiltable to be shown to the end user, suitable!

$user = new User();
$user->query();

/*
  assume active is defined as:

    public $active = array(
        'type'   => CHAR,
        'opions' => array(
            'Y' => 'Yes',
            'N' => 'No',
        ),
    );
 */

echo $user->active; // will echo 'Y' or 'N'

MakeReadable::scalar($user);

echo $user->active; // will echo 'Yes' or 'No'

/* makes scalar properties readable */
MakeReadable::scalars($user);
/* makes object properties readable */
MakeReadable::objects($user);
/* makes object list properties readable */
MakeReadable::objectLists($user);
/* makes all properties readable */
MakeReadable::render($user);

Usage: when iterating over model data after a query in a template, use the MakeReadable methods inside the afterNext event to make the data readable before showing.

Note! model data will not be suitable for adding/updating/querying... after becoming readable!

Example

Models
TOP