ice framework documentation ice doc v1.10.1
Class Ice Mvc

Model

    
namespace Ice\Mvc;

use Ice\Di;
use Ice\Arr;
use Ice\Exception;
use Ice\Validation;

/**
 * Model connects business objects and database tables to create a persistable domain model where logic and data are
 * presented in one wrapping (ORM & ODM).
 *
 * @package     Ice/Db
 * @category    Component
 * @author      Ice Team
 * @copyright   (c) 2014-2023 Ice Team
 * @license     http://iceframework.org/license
 */
abstract class Model extends Arr
{
    protected di { get };
    protected db { get };
    protected service = "db";
    protected from { set };
    protected primary { set, get };
    protected autoincrement = true { set };
    protected filters = [] { set, get };
    protected fields = [] { set, get };
    protected validation { set, get };
    protected relations = [] { get };
    protected labels = [] { set };
    protected rules = [];
    protected messages = [] { get, set };

    // this model has been loaded from service
    protected isLoaded = null;

    const BELONGS_TO = 1;
    const HAS_ONE = 2;
    const HAS_MANY = 3;

    /**
     * Model constructor. Fetch Di and set it as a property.
     *
     * @param mixed filters
     * @param array data
     * @param array options Options to limit/group/orderby results when filters is not null
     */
    public function __construct(var filters = null, array data = [], array options = [])
    {
        var di;

        let di = Di::$fetch(),
            this->di = di,
            this->db = di->get(this->service);

        let data = array_merge(array_fill_keys(this->fields, null), data);

        parent::__construct(data);

        if !this->from {
            let this->from = uncamelize(get_class_ns(this));
        }

        if !this->primary {
            let this->primary = this->db->getId();
        }

        if method_exists(this, "onConstruct") {
            this->{"onConstruct"}();
        }

        if filters {
            this->loadOne(filters, options);
        }

        if method_exists(this, "initialize") {
            this->{"initialize"}();
        }
    }

    /**
     * Get the id.
     *
     * @return mixed
     */
    public function getId()
    {
        if typeof this->primary == "array" {
            return this->only(this->primary);
        } else {
            return this->get(this->primary);
        }
    }

    /**
     * Get the id key depending on db driver.
     *
     * @return string
     */
    public function getIdKey() -> string
    {
        return this->db->getId();
    }

    /**
     * Get the date time object.
     *
     * @param mixed key
     * @param boolean model
     * @return object
     */
    public function getDateTime(key, model = false)
    {
        var value;
        if this->has(key) {
            let value = this->get(key);
        } else {
            let value = key;
        }

        return this->db->getDateTime(value, model);
    }

    /**
     * Load one result to the current object.
     *
     * @param mixed filters
     * @param array options Options to limit/group/orderby results
     * @return this|false
     */
    public function loadOne(var filters, array options = [])
    {
        var result;

        if empty filters {
            return false;
        }

        let result = this->db->findOne(this->from, filters, options);

        if result {
            this->merge(result->all());
            let this->isLoaded = true;
            return this;
        } else {
            let this->isLoaded = false;
            return false;
        }
    }

    /**
     * Load results to the current object.
     *
     * @param mixed filters
     * @param array options
     * @return object Arr
     */
    public function load(var filters, array options = [])
    {
        var result, instances, data, model;

        let result = this->db->find(this->from, filters, options),
            instances = [];

        if result->count() {
            for data in iterator(result) {
                let model = create_instance_params(get_called_class(), [null, data]),
                    model->isLoaded = true,
                    instances[] = model;
            }
        }
        return new Arr(instances);
    }

    /**
     * Allows to query one record that match the specified conditions.
     *
     * 

     *  //Get the user from users by id 2
     *  $user = Users::findOne(2);
     *  echo "The user name is ", $user->username;
     *
     *  //Get one active user with age > 18
     *  $user = Users::findOne(array("status" => 1, "age" => array(">" => 18)));
     * 
* * @param array filters * @param array options * @return this|false */ public static function findOne(var filters = null, array options = []) { var model; let model = create_instance_params(get_called_class(), [filters, [], options]); return model->isLoaded ? model : false; } /** * Allows to query all records that match the specified conditions. * *

     *  //Get all active users with age > 18
     *  $user = Users::find(array("status" => 1, "age" => array(">" => 18)));
     * 
* * @param array filters * @param array options * @return object Arr */ public static function find(var filters = null, array options = []) { var model; let model = create_instance(get_called_class()); return model->load(filters, options); } /** * Prepare fields for validation on create/update. * * @param mixed fields Fields to save or valid fields * @param boolean primary Keep primary key * @return array */ protected function fields(var fields = [], boolean primary = true) { // Check if model has defined valid fields if empty this->fields { // No defined model's fields // Check if fields param has any elements if empty fields { // Get all model's data as fields let fields = this->all(); } else { // Get only fields from method parameter // Check if fields param is associative or sequential if array_filter(array_keys(fields), "is_string") { // Merge model data with fields values let fields = array_merge(this->all(), fields); } else { // Use fields as only valid keys let fields = array_intersect_key(this->all(), array_flip(fields)); } } } else { // Only valid model's fields // Check if fields param has any elements if empty fields { // Get all valid model's data as fields let fields = array_intersect_key(this->all(), array_flip(this->fields)); } else { // Get only fields from method parameter // Check if fields param is associative or sequential if array_filter(array_keys(fields), "is_string") { // Merge model data with fields values, get only valid model's fields let fields = array_intersect_key(array_merge(this->all(), fields), array_flip(this->fields)); } else { // Use fields as only valid keys, get only valid model's fields let fields = array_intersect_key(this->all(), array_flip(fields), array_flip(this->fields)); } } } // Remove primary key if !primary { var key, keys = []; if typeof this->primary == "array" { for key in this->primary { let keys[] = key; } } else { let keys[] = this->primary; } for key in keys { unset fields[key]; } } return fields; } /** * Insert a new object to the database. * *

     *  //Creating a new user
     *  $user = new Users();
     *  $user->lastname = "Kowalski";
     *  $user->status = 1;
     *  $user->create();
     * 
* * @param array fields Fields to save or valid fields * @param object extra Validation for fields such as a CSRF token, password verification, or a CAPTCHA * @return null|boolean If validate fail return null, else return insert status */ public function create(var fields = [], extra = null) { var status; this->setData(this->fields(fields, !this->autoincrement)); if extra { extra->validate(); let this->messages = extra->getMessages()->all(); } else { let this->messages = []; } this->di->applyHook("model.before.validate", [this], this); // Run validation if rules or validation is specified if !empty this->rules || typeof this->validation == "object" && (this->validation instanceof Validation) { if !(typeof this->validation == "object" && (this->validation instanceof Validation)) { let this->validation = new Validation(); } if !this->validation->getRules() { // Resolve the rules this->validation->rules(this->rules); } this->validation->setFilters(this->filters); this->validation->setLabels(this->labels); this->validation->validate(this->getData()); // Replace values by validation values (with applied filters) this->merge(this->validation->getValues()); if !this->validation->valid() { let this->messages = array_merge(this->messages, this->validation->getMessages()->all()); } } this->di->applyHook("model.after.validate", [this], this); if !empty this->messages { return null; } this->di->applyHook("model.before.create", [this], this); let status = this->db->insert(this->from, this->getData()); if status { let this->isLoaded = true; if this->autoincrement { this->set(this->db->getId(), this->db->getLastInsertId()); } } this->di->applyHook("model.after.create", [this], this); return status; } /** * Update an existing object in the database. * *

     *  //Updating a user last name
     *  $user = Users::findOne(100);
     *  $user->lastname = "Nowak";
     *  $user->update();
     * 
* * @param array fields Fields to save or valid fields * @param object extra Validation for fields such as a CSRF token, password verification, or a CAPTCHA * @return null|boolean If validate fail return null, else return update status */ public function update(var fields = [], extra = null) { var data, status, primary, key; let data = this->getData(), primary = []; if typeof this->primary == "array" { for key in this->primary { let primary[key] = this->get(key); } } else { let primary = [this->primary: this->get(this->primary)]; } this->setData(this->fields(fields)); if extra { extra->validate(); let this->messages = extra->getMessages()->all(); } else { let this->messages = []; } this->di->applyHook("model.before.validate", [this], this); if typeof this->validation == "object" && (this->validation instanceof Validation) { this->validation->validate(this->getData()); // Replace values by validation values (with applied filters) this->merge(this->validation->getValues()); if !this->validation->valid() { let this->messages = array_merge(this->messages, this->validation->getMessages()->all()); } } this->di->applyHook("model.after.validate", [this], this); if !empty this->messages { // Rollback changes and restore old data this->setData(data); return null; } this->di->applyHook("model.before.update", [this], this); let status = this->db->update(this->from, primary, this->fields(this->getData(), !this->autoincrement)); if !status { // Rollback changes and restore old data this->setData(data); } else { let this->isLoaded = true; // changes and update old data this->setData(array_merge(data, fields)); } this->di->applyHook("model.after.update", [this], this); return status; } /** * Inserts or updates a model instance. Returning true on success or false otherwise. * *

     *  //Creating a new user
     *  $user = new Users();
     *  $user->lastname = "Kowalski";
     *  $user->status = 1;
     *  $user->save();
     *
     *  //Updating a user last name
     *  $user = Users::findOne(100);
     *  $user->lastname = "Nowak";
     *  $user->save();
     * 
* * @param array fields * @param Validation extra * @return null|boolean If validate fail return null, else return save status */ public function save(var fields = [], extra = null) { if this->exists() { return this->update(fields, extra); } else { return this->create(fields, extra); } } /** * Removes a model instance(s). Returning true on success or false otherwise. * *

     *  //Remove current user
     *  $user = Users::findOne(100);
     *  $user->delete();
     *
     *  //Remove all unactive users
     *  $status = (new Users())->delete(["status" => 0]);
     * 
* * @param filters * @return boolean */ public function delete(var filters = []) { var key, status; if !filters { let filters = []; if typeof this->primary == "array" { for key in this->primary { let filters[key] = this->get(key); } } else { let filters = [this->primary: this->get(this->primary)]; } } let status = this->db->delete(this->from, filters); if status { // this model doesn't exist in db let this->isLoaded = false; } return status; } /** * Get the record if exist. * * @param mixed filters * @return Model|false */ public function exists(var filters = []) { var key; if this->isLoaded !== null { return this->isLoaded ? this : false; } if !filters { let filters = []; if typeof this->primary == "array" { for key in this->primary { if this->has(key) && this->get(key) { let filters[key] = this->get(key); } else { return false; } } } else { if this->has(this->primary) && this->get(this->primary) { let filters = [this->primary: this->get(this->primary)]; } else { return false; } } } return this->loadOne(filters); } /** * Check whether model is loaded. * * @return boolean */ public function loaded() -> boolean { return this->isLoaded ? true : false; } /** * Get the last Db error. * * @return mixed */ public function getError() { return this->db->getError(); } /** * Setup a relation reverse 1-1 between two models. * *

     *  class Posts extends Model
     *  {
     *      public function initialize()
     *      {
     *          //Relation with user, be able to get post's author
     *          $this->belongsTo('user_id', __NAMESPACE__ . '\Users', 'id', ['alias' => 'User']);
     *      }
     *  }
     *
     *  //Get post's author
     *  $post = Posts::findOne(100);
     *  echo $post->getUser()->username;
     * 
* * @param string field * @param string referenceModel * @param string referencedField * @param array options * @return object Model */ public function belongsTo(string field, string referenceModel, string referencedField, array options = []) { var alias; if !fetch alias, options["alias"] { let alias = referenceModel; } let this->relations[alias] = [ "type": self::BELONGS_TO, "field": field, "referenceModel": referenceModel, "referencedField": referencedField, "options": options ]; return this; } /** * Setup a 1-1 relation between two models * *

     *  class Users extends Model
     *  {
     *      public function initialize()
     *      {
     *          $this->hasOne('id', __NAMESPACE__ . '\UsersDescriptions', 'user_id', ['alias' => 'Description']);
     *      }
     *  }
     * 
* * @param string field * @param string referenceModel * @param string referencedField * @param array options * @return object Model */ public function hasOne(string field, string referenceModel, string referencedField, array options = []) { var alias; if !fetch alias, options["alias"] { let alias = referenceModel; } let this->relations[alias] = [ "type": self::HAS_ONE, "field": field, "referenceModel": referenceModel, "referencedField": referencedField, "options": options ]; return this; } /** * Setup a relation 1-n between two models. * *

     *  class Users extends Model
     *  {
     *      public function initialize()
     *      {
     *          //Relation with posts, be able to get user's posts
     *          $this->hasMany('id', __NAMESPACE__ . '\Posts', 'user_id', ['alias' => 'Posts']);
     *      }
     *  }
     *
     *  //Get user's posts
     *  $user = Users::findOne(2);
     *  foreach ($user->getPosts() as $post) {
     *      echo $post->title;
     *  }
     * 
* * @param string field * @param string referenceModel * @param string referencedField * @param array options * @return object Model */ public function hasMany(string field, string referenceModel, string referencedField, array options = []) { var alias; if !fetch alias, options["alias"] { let alias = referenceModel; } let this->relations[alias] = [ "type": self::HAS_MANY, "field": field, "referenceModel": referenceModel, "referencedField": referencedField, "options": options ]; return this; } /** * Get related models. * * @param string alias * @param array filters * @param array options */ public function getRelated(string alias, array filters = [], array options = []) { var relation, field, referenceModel, referencedField, result; if !fetch relation, this->relations[alias] { throw new Exception(sprintf("Alias '%s' not found", alias)); } fetch field, relation["field"]; fetch referenceModel, relation["referenceModel"]; fetch referencedField, relation["referencedField"]; switch relation["type"] { case self::BELONGS_TO: case self::HAS_ONE: let filters[referencedField] = this->{field}, result = create_instance_params(referenceModel, [filters, [], options]); if !result->loaded() { return false; } return result; case self::HAS_MANY: let filters[referencedField] = this->{field}, result = {referenceModel}::find(filters, options); return result; } } /** * Get rules for validation. * *

     *  // Get rules for one field
     *  $this->getRules('password');
     *
     *  // Get rules for multiple fields
     *  $this->getRules(['fullName', 'about']);
     *
     *  // Get all rules
     *  $this->getRules();
     * 
* * @param mixed fields * @return mixed */ public function getRules(fields = null) { var rules, field; if fields { if typeof fields == "array" { let rules = []; for field in fields { if isset this->rules[field] { let rules[field] = this->rules[field]; } } return rules; } elseif typeof fields == "string" && isset this->rules[fields] { return this->rules[fields]; } return null; } return this->rules; } /** * Set rules for validation. * * @param array rules * @param boolean merge * @return object Model */ public function setRules(array! rules = [], boolean merge = true) { if merge { let this->rules = array_merge(this->rules, rules); } else { let this->rules = rules; } return this; } /** * Serialize the model's data. * * @return array */ public function __serialize() -> array { return [ "data": base64_encode(serialize(this->data)) ]; } /** * Unserialize and set the data. * * @param array serialized * @return object Model */ public function __unserialize(array serialized) { this->__construct(); let this->data = unserialize(base64_decode(serialized["data"])); return this; } /** * Magic call to get related models. */ public function __call(string method, arguments = null) { var filters, options; if starts_with(method, "get") { if !fetch filters, arguments[0] { let filters = []; } if !fetch options, arguments[1] { let options = []; } return this->getRelated(ucfirst(substr(method, 3)), filters, options); } // The method doesn't exist throw an exception throw new Exception(sprintf("The method '%s' doesn't exist", method)); } }