├── LICENSE
├── README.md
├── composer.json
├── example
    ├── README.md
    ├── requestLog
    │   ├── My_model.php
    │   └── README.md
    └── userACL
    │   ├── My_model.php
    │   └── README.md
└── src
    └── Model.php
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2017 Nick Tsai
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
   1 | 
   2 |     
   3 |          4 |     
   5 |
   4 |     
   5 |     
CodeIgniter Model
   6 |     
   7 | 
   8 | 
   9 | CodeIgniter 3 Active Record (ORM) Standard Model supported Read & Write Connections
  10 | 
  11 | [](https://packagist.org/packages/yidas/codeigniter-model)
  12 | [](https://packagist.org/packages/yidas/codeigniter-model)
  13 | 
  14 | This ORM Model extension is collected into [yidas/codeigniter-pack](https://github.com/yidas/codeigniter-pack) which is a complete solution for Codeigniter framework.
  15 | 
  16 | FEATURES
  17 | --------
  18 | 
  19 | - ***[ORM](#active-record-orm)** Model with **Elegant patterns** as Laravel Eloquent ORM & Yii2 Active Record*
  20 | 
  21 | - ***[CodeIgniter Query Builder](#find)** integration*
  22 | 
  23 | - ***[Timestamps Behavior](#timestamps)** & **[Validation](#validation)** & **[Soft Deleting](#soft-deleted)** & **[Query Scopes](#query-scopes)** support*
  24 | 
  25 | - ***[Read & Write Splitting](#read--write-connections)** for Replications*
  26 | 
  27 | This package provide Base Model which extended `CI_Model` and provided full CRUD methods to make developing database interactions easier and quicker for your CodeIgniter applications.
  28 | 
  29 | OUTLINE
  30 | -------
  31 | 
  32 | - [Demonstration](#demonstration)
  33 | - [Requirements](#requirements)
  34 | - [Installation](#installation)
  35 | - [Configuration](#configuration)
  36 | - [Defining Models](#defining-models)
  37 |   - [Table Names](#table-names)
  38 |   - [Primary Keys](#primary-keys)
  39 |   - [Timestamps](#timestamps)
  40 |   - [Database Connection](#database-connection)
  41 |   - [Other settings](#other-settings)
  42 | - [Basic Usage](#basic-usage)
  43 |   - [Methods](#methods)
  44 |     - [find()](#find)
  45 |       - [Query Builder Implementation](#query-builder-implementation)
  46 |     - [reset()](#reset)
  47 |     - [insert()](#insert)
  48 |     - [batchInsert()](#batchinsert)
  49 |     - [update()](#update)
  50 |     - [batchUpdate()](#batchupdate)
  51 |     - [replace()](#replace)
  52 |     - [delete()](#delete)
  53 |     - [getLastInsertID()](#getlastinsertid)
  54 |     - [getAffectedRows()](#getaffectedrows)
  55 |     - [count()](#count)
  56 |     - [setAlias()](#setalias)
  57 | - [Active Record (ORM)](#active-record-orm)
  58 |   - [Inserts](#inserts)
  59 |   - [Updates](#updates)
  60 |   - [Deletes](#deletes)
  61 |   - [Accessing Data](#accessing-data)
  62 |   - [Relationships](#relationships)
  63 |   - [Methods](#methods-1)
  64 |     - [findone()](#findone)
  65 |     - [findAll()](#findall)
  66 |     - [save()](#save)
  67 |     - [beforeSave()](#beforesave)
  68 |     - [afterSave()](#afterave)
  69 |     - [hasOne()](#hasone)
  70 |     - [hasMany()](#hasmany)
  71 |     - [toArray()](#toarray)
  72 | - [Soft Deleted](#soft-deleted)
  73 |   - [Configuration](#configuration-1)
  74 |   - [Methods](#method-2)
  75 | - [Query Scopes](#query-scopes)
  76 |   - [Configuration](#configuration-2)
  77 |   - [Methods](#method-3)
  78 | - [Validation](#validation)
  79 |   - [Validating Input](#validating-input)
  80 |     - [validate()](#validate)
  81 |     - [getErrors()](#geterrors)
  82 |   - [Declaring Rules](#declaring-rules)
  83 |     - [rules()](#rules)
  84 |     - [Error Message with Language](#error-message-with-language)
  85 |   - [Filters](#filters)
  86 |     - [Filters()](#filters-1)
  87 | - [Read & Write Connections](#read--write-connections)
  88 |   - [Configuration](#configuration-3)
  89 |   - [Load Balancing for Databases](#load-balancing-for-databases)
  90 |   - [Reconnection](#reconnection)
  91 | - [Pessimistic Locking](#pessimistic-locking)
  92 | - [Helpers](#helpers)
  93 |   - [indexBy()](#indexby)
  94 |   
  95 | ---
  96 | 
  97 | DEMONSTRATION
  98 | -------------
  99 | 
 100 | ### ActiveRecord (ORM)
 101 | 
 102 | ```php
 103 | $this->load->model('Posts_model');
 104 | 
 105 | // Create an Active Record
 106 | $post = new Posts_model;
 107 | $post->title = 'CI3'; // Equivalent to `$post['title'] = 'CI3';`
 108 | $post->save();
 109 | 
 110 | // Update the Active Record found by primary key
 111 | $post = $this->Posts_model->findOne(1);
 112 | if ($post) {
 113 |     $oldTitle = $post->title; // Equivalent to `$oldTitle = $post['title'];`
 114 |     $post->title = 'New CI3';
 115 |     $post->save();
 116 | }
 117 | ```
 118 | 
 119 | > The pattern is similar to [Yii2 Active Record](https://www.yiiframework.com/doc/guide/2.0/en/db-active-record#active-record) and [Laravel Eloquent](https://laravel.com/docs/5.8/eloquent#inserting-and-updating-models)
 120 | 
 121 | ### Find with Query Builder
 122 | 
 123 | Start to use CodeIgniter Query Builder from `find()` method, the Model will automatically load its own database connections and data tables.
 124 | 
 125 | ```php
 126 | $records = $this->Posts_model->find()
 127 |     ->select('*')
 128 |     ->where('is_public', '1')
 129 |     ->limit(25)
 130 |     ->order_by('id')
 131 |     ->get()
 132 |     ->result_array();
 133 | ```
 134 | 
 135 | ### CRUD
 136 | 
 137 | ```php
 138 | $result = $this->Posts_model->insert(['title' => 'Codeigniter Model']);
 139 | 
 140 | // Find out the record which just be inserted
 141 | $record = $this->Posts_model->find()
 142 |   ->order_by('id', 'DESC')
 143 |   ->get()
 144 |   ->row_array();
 145 |   
 146 | // Update the record
 147 | $result = $this->Posts_model->update(['title' => 'CI3 Model'], $record['id']);
 148 | 
 149 | // Delete the record
 150 | $result = $this->Posts_model->delete($record['id']);
 151 | ```
 152 | 
 153 | ---
 154 | 
 155 | REQUIREMENTS
 156 | ------------
 157 | 
 158 | This library requires the following:
 159 | 
 160 | - PHP 5.4.0+
 161 | - CodeIgniter 3.0.0+
 162 | 
 163 | ---
 164 | 
 165 | INSTALLATION
 166 | ------------
 167 | 
 168 | Run Composer in your Codeigniter project under the folder `\application`:
 169 | 
 170 |     composer require yidas/codeigniter-model
 171 |     
 172 | Check Codeigniter `application/config/config.php`:
 173 | 
 174 | ```php
 175 | $config['composer_autoload'] = TRUE;
 176 | ```
 177 |     
 178 | > You could customize the vendor path into `$config['composer_autoload']`
 179 | 
 180 | ---
 181 | 
 182 | CONFIGURATION
 183 | -------------
 184 | 
 185 | After installation, `yidas\Model` class is ready to use. Simply, you could create a model to extend the `yidas\Model` directly:
 186 | 
 187 | ```php
 188 | class Post_model extends yidas\Model {}
 189 | ```
 190 | 
 191 | After that, this model is ready to use for example: `$this->PostModel->findOne(123);`
 192 | 
 193 | However, the schema of tables such as primary key in your applicaiton may not same as default, and it's annoying to defind repeated schema for each model. We recommend you to make `My_model` to extend `yidas\Model` instead.
 194 | 
 195 | ### Use My_model to Extend Base Model for every Models
 196 | 
 197 | You could use `My_model` to extend `yidas\Model`, then make each model to extend `My_model` in Codeigniter application.
 198 | 
 199 | *1. Create `My_model` extended `yidas\Model` with configuration for fitting your common table schema:*
 200 | 
 201 | ```php
 202 | class My_model extends yidas\Model
 203 | {
 204 |     protected $primaryKey = 'sn';
 205 |     const CREATED_AT = 'created_time';
 206 |     const UPDATED_AT = 'updated_time';
 207 |     // Customized Configurations for your app...
 208 | }
 209 | ```
 210 | 
 211 | *2. Create each Model extended `My_model` in application with its own table configuration:*
 212 | 
 213 | ```php
 214 | class Post_model extends My_model
 215 | {
 216 |     protected $table = "post_table";
 217 | }
 218 | ```
 219 | 
 220 | *3. Use each extended Model with library usages:*
 221 | 
 222 | ```php
 223 | $this->load->model('post_model', 'PostModel');
 224 | 
 225 | $post = $this->PostModel->findOne(123);
 226 | ```
 227 | 
 228 | [My_model Example with Document](https://github.com/yidas/codeigniter-model/tree/master/example)
 229 | 
 230 | ---
 231 | 
 232 | DEFINING MODELS
 233 | ---------------
 234 | 
 235 | To get started, let's create an model extends `yidas\Model` or through `My_model`, then define each model suitably.
 236 | 
 237 | ### Table Names
 238 | 
 239 | By convention, the "snake case" with lowercase excluded `_model` postfix of the class name will be used as the table name unless another name is explicitly specified. So, in this case, Model will assume the `Post_model` model stores records in the `post` table. You may specify a custom table by defining a table property on your model:
 240 | 
 241 | ```php
 242 | // class My_model extends yidas\Model
 243 | class Post_model extends My_model
 244 | {
 245 |     protected $table = "post_table";
 246 | }
 247 | ```
 248 | 
 249 | > You could set table alias by defining `protected $alias = 'A1';` for model.
 250 | 
 251 | #### Table Name Guessing Rule
 252 | 
 253 | In our pattern, The naming between model class and table is the same, with supporting no matter singular or plural names:
 254 | 
 255 | |Model Class Name|Table Name|
 256 | |--|--|
 257 | |Post_model|post|
 258 | |Posts_model|posts|
 259 | |User_info_model|user_info|
 260 | 
 261 | #### Get Table Name
 262 | 
 263 | You could get table name from each Model:
 264 | 
 265 | ```php
 266 | $tableName = $this->PostModel->getTable();
 267 | ```
 268 | 
 269 | 
 270 | 
 271 | ### Primary Keys
 272 | 
 273 | You may define a protected `$primaryKey` property to override this convention:
 274 | 
 275 | ```php
 276 | class My_model extends yidas\Model
 277 | {
 278 |     protected $primaryKey = "sn";
 279 | }
 280 | ```
 281 | 
 282 | > Correct primary key setting of Model is neceesary for Active Record (ORM). 
 283 | 
 284 | ### Timestamps
 285 | 
 286 | By default, Model expects `created_at` and `updated_at` columns to exist on your tables. If you do not wish to have these columns automatically managed by base Model, set the `$timestamps` property on your model as `false`:
 287 | 
 288 | ```php
 289 | class My_model extends yidas\Model
 290 | {
 291 |     protected $timestamps = false;
 292 | }
 293 | ```
 294 | 
 295 | If you need to customize the format of your timestamps, set the `$dateFormat` property on your model. This property determines how date attributes are stored in the database:
 296 | 
 297 | ```php
 298 | class My_model extends yidas\Model
 299 | {
 300 |     /**
 301 |      * Date format for timestamps.
 302 |      *
 303 |      * @var string unixtime(946684800)|datetime(2000-01-01 00:00:00)
 304 |      */
 305 |     protected $dateFormat = 'datetime';
 306 | }
 307 | ```
 308 | 
 309 | If you need to customize the names of the columns used to store the timestamps, you may set the `CREATED_AT` and `UPDATED_AT` constants in your model:
 310 | 
 311 | ```php
 312 | class My_model extends yidas\Model
 313 | {
 314 |     const CREATED_AT = 'created_time';
 315 |     const UPDATED_AT = 'updated_time';
 316 | }
 317 | ```
 318 | 
 319 | Also, you could customized turn timestamps behavior off for specified column by assigning as empty:
 320 | 
 321 | ```php
 322 | class My_model extends yidas\Model
 323 | {
 324 |     const CREATED_AT = 'created_time';
 325 |     const UPDATED_AT = NULL;
 326 | }
 327 | ```
 328 | 
 329 | ### Database Connection
 330 | 
 331 | By default, all models will use the default database connection `$this->db` configured for your application. If you would like to specify a different connection for the model, use the `$database` property:
 332 | 
 333 | ```php
 334 | class My_model extends yidas\Model
 335 | {
 336 |     protected $database = 'database2';
 337 | }
 338 | ```
 339 | 
 340 | > More Database Connection settings: [Read & Write Connections](#read--write-connections)
 341 | 
 342 | 
 343 | ### Other settings
 344 | 
 345 | ```php
 346 | class My_model extends yidas\Model
 347 | {
 348 |     // Enable ORM property check for write
 349 |     protected $propertyCheck = true;
 350 | }
 351 | ```
 352 | 
 353 | ---
 354 | 
 355 | BASIC USAGE
 356 | -----------
 357 | 
 358 | Above usage examples are calling Models out of model, for example in controller:
 359 | 
 360 | ```php
 361 | $this->load->model('post_model', 'Model');
 362 | ```
 363 | 
 364 | If you call methods in Model itself, just calling `$this` as model. For example, `$this->find()...` for `find()`;
 365 | 
 366 | ### Methods
 367 | 
 368 | #### `find()`
 369 | 
 370 | Create an existent CI Query Builder instance with Model features for query purpose.
 371 | 
 372 | ```php
 373 | public CI_DB_query_builder find(boolean $withAll=false)
 374 | ```
 375 | 
 376 | *Example:*
 377 | ```php
 378 | $records = $this->Model->find()
 379 |     ->select('*')
 380 |     ->where('is_public', '1')
 381 |     ->limit(25)
 382 |     ->order_by('id')
 383 |     ->get()
 384 |     ->result_array();
 385 | ```
 386 | 
 387 | ```php
 388 | // Without any scopes & conditions for this query
 389 | $records = $this->Model->find(true)
 390 |     ->where('is_deleted', '1')
 391 |     ->get()
 392 |     ->result_array();
 393 |     
 394 | // This is equal to find(true) method
 395 | $this->Model->withAll()->find();
 396 | ```
 397 | 
 398 | > After starting `find()` from a model, it return original `CI_DB_query_builder` for chaining. The query builder could refer [CodeIgniter Query Builder Class Document](https://www.codeigniter.com/userguide3/database/query_builder.html)
 399 | 
 400 | ##### Query Builder Implementation
 401 | 
 402 | You could assign Query Builder as a variable to handle add-on conditions instead of using `$this->Model->getBuilder()`.
 403 | 
 404 | ```php
 405 | $queryBuilder = $this->Model->find();
 406 | if ($filter) {
 407 |     $queryBuilder->where('filter', $filter);
 408 | }
 409 | $records = $queryBuilder->get()->result_array();
 410 | ```
 411 | 
 412 | #### `reset()`
 413 | 
 414 | reset an CI Query Builder instance with Model.
 415 | 
 416 | ```php
 417 | public self reset()
 418 | ```
 419 | 
 420 | *Example:*
 421 | ```php
 422 | $this->Model->reset()->find();
 423 | ```
 424 | 
 425 | #### `insert()`
 426 | 
 427 | Insert a row with Timestamps feature into the associated database table using the attribute values of this record.
 428 | 
 429 | ```php
 430 | public boolean insert(array $attributes, $runValidation=true)
 431 | ```
 432 | 
 433 | *Example:*
 434 | ```php
 435 | $result = $this->Model->insert([
 436 |     'name' => 'Nick Tsai',
 437 |     'email' => 'myintaer@gmail.com',
 438 | ]);
 439 | ```
 440 | 
 441 | #### `batchInsert()`
 442 | 
 443 | Insert a batch of rows with Timestamps feature into the associated database table using the attribute values of this record.
 444 | 
 445 | ```php
 446 | public integer batchInsert(array $data, $runValidation=true)
 447 | ```
 448 | 
 449 | *Example:*
 450 | ```php
 451 | $result = $this->Model->batchInsert([
 452 |      ['name' => 'Nick Tsai', 'email' => 'myintaer@gmail.com'],
 453 |      ['name' => 'Yidas', 'email' => 'service@yidas.com']
 454 | ]);
 455 | ```
 456 | 
 457 | #### `replace()`
 458 | 
 459 | Replace a row with Timestamps feature into the associated database table using the attribute values of this record.
 460 | 
 461 | ```php
 462 | public boolean replace(array $attributes, $runValidation=true)
 463 | ```
 464 | 
 465 | *Example:*
 466 | ```php
 467 | $result = $this->Model->replace([
 468 |     'id' => 1,
 469 |     'name' => 'Nick Tsai',
 470 |     'email' => 'myintaer@gmail.com',
 471 | ]);
 472 | ```
 473 | 
 474 | #### `update()`
 475 | 
 476 | Save the changes with Timestamps feature to the selected record(s) into the associated database table.
 477 | 
 478 | ```php
 479 | public boolean update(array $attributes, array|string $condition=NULL, $runValidation=true)
 480 | ```
 481 | 
 482 | *Example:*
 483 | ```php
 484 | $result = $this->Model->update(['status'=>'off'], 123)
 485 | ```
 486 | 
 487 | ```php
 488 | // Find conditions first then call again
 489 | $this->Model->find()->where('id', 123);
 490 | $result = $this->Model->update(['status'=>'off']);
 491 | ```
 492 | 
 493 | ```php
 494 | // Counter set usage equal to `UPDATE mytable SET count = count+1 WHERE id = 123`
 495 | $this->Model->getDB()->set('count','count + 1', FALSE);
 496 | $this->Model->find()->where('id', 123);
 497 | $result = $this->Model->update([]);
 498 | ```
 499 | 
 500 | > Notice: You need to call `update` from Model but not from CI-DB builder chain, the wrong sample code: 
 501 | > 
 502 | > `$this->Model->find()->where('id', 123)->update('table', ['status'=>'off']);`
 503 | 
 504 | #### `batchUpdate()`
 505 | 
 506 | Update a batch of update queries into combined query strings.
 507 | 
 508 | ```php
 509 | public integer batchUpdate(array $dataSet, boolean $withAll=false, interger $maxLength=4*1024*1024, $runValidation=true)
 510 | ```
 511 | 
 512 | *Example:*
 513 | ```php
 514 | $result = $this->Model->batchUpdate([
 515 |     [['title'=>'A1', 'modified'=>'1'], ['id'=>1]],
 516 |     [['title'=>'A2', 'modified'=>'1'], ['id'=>2]],
 517 | ]);
 518 | ```
 519 | 
 520 | #### `delete()`
 521 | 
 522 | Delete the selected record(s) with Timestamps feature into the associated database table.
 523 | 
 524 | ```php
 525 | public boolean delete(array|string $condition=NULL, boolean $forceDelete=false, array $attributes=[])
 526 | ```
 527 | 
 528 | *Example:*
 529 | ```php
 530 | $result = $this->Model->delete(123)
 531 | ```
 532 | 
 533 | ```php
 534 | // Find conditions first then call again
 535 | $this->Model->find()->where('id', 123);
 536 | $result = $this->Model->delete();
 537 | ```
 538 | 
 539 | ```php
 540 | // Force delete for SOFT_DELETED mode 
 541 | $this->Model->delete(123, true);
 542 | ```
 543 | 
 544 | #### `getLastInsertID()`
 545 | 
 546 | Get the insert ID number when performing database inserts.
 547 | 
 548 | 
 549 | *Example:*
 550 | ```php
 551 | $result = $this->Model->insert(['name' => 'Nick Tsai']);
 552 | $lastInsertID = $this->Model->getLastInsertID();
 553 | ```
 554 | 
 555 | #### `getAffectedRows()`
 556 | 
 557 | Get the number of affected rows when doing “write” type queries (insert, update, etc.).
 558 | 
 559 | ```php
 560 | public integer|string getLastInsertID()
 561 | ```
 562 | 
 563 | *Example:*
 564 | ```php
 565 | $result = $this->Model->update(['name' => 'Nick Tsai'], 32);
 566 | $affectedRows = $this->Model->getAffectedRows();
 567 | ```
 568 | 
 569 | #### `count()`
 570 | 
 571 | Get count from query
 572 | 
 573 | ```php
 574 | public integer count(boolean $resetQuery=true)
 575 | ```
 576 | 
 577 | *Example:*
 578 | ```php
 579 | $result = $this->Model->find()->where("age <", 20);
 580 | $totalCount = $this->Model->count();
 581 | ```
 582 | 
 583 | #### `setAlias()`
 584 | 
 585 | Set table alias
 586 | 
 587 | ```php
 588 | public self setAlias(string $alias)
 589 | ```
 590 | 
 591 | *Example:*
 592 | ```php
 593 | $query = $this->Model->setAlias("A1")
 594 |     ->find()
 595 |     ->join('table2 AS A2', 'A1.id = A2.id');
 596 | ```
 597 | 
 598 | ---
 599 | 
 600 | ACTIVE RECORD (ORM)
 601 | -------------------
 602 | 
 603 | Active Record provides an object-oriented interface for accessing and manipulating data stored in databases. An Active Record Model class is associated with a database table, an Active Record instance corresponds to a row of that table, and an attribute of an Active Record instance represents the value of a particular column in that row.
 604 | 
 605 | > Active Record (ORM) supported events such as timestamp for insert and update.
 606 | 
 607 | ### Inserts
 608 | 
 609 | To create a new record in the database, create a new model instance, set attributes on the model, then call the `save` method:
 610 | 
 611 | ```php
 612 | $this->load->model('Posts_model');
 613 | 
 614 | $post = new Posts_model;
 615 | $post->title = 'CI3';
 616 | $result = $post->save();
 617 | ```
 618 | 
 619 | ### Updates
 620 | 
 621 | The `save` method may also be used to update models that already exist in the database. To update a model, you should retrieve it, set any attributes you wish to update, and then call the `save` method:
 622 | 
 623 | ```php
 624 | $this->load->model('Posts_model');
 625 | 
 626 | $post = $this->Posts_model->findOne(1);
 627 | if ($post) {
 628 |     $post->title = 'New CI3';
 629 |     $result = $post->save();
 630 | }
 631 | ```
 632 | 
 633 | ### Deletes
 634 | 
 635 | To delete a active record, call the `delete` method on a model instance:
 636 | 
 637 | ```php
 638 | $this->load->model('Posts_model');
 639 | 
 640 | $post = $this->Posts_model->findOne(1);
 641 | $result = $post->delete();
 642 | ```
 643 | 
 644 | > `delete()` supports soft deleted and points to self if is Active Record.
 645 | 
 646 | ### Accessing Data
 647 | 
 648 | You could access the column values by accessing the attributes of the Active Record instances likes `$activeRecord->attribute`, or get by array key likes `$activeRecord['attribute']`.
 649 | 
 650 | ```php
 651 | $this->load->model('Posts_model');
 652 | 
 653 | // Set attributes
 654 | $post = new Posts_model;
 655 | $post->title = 'CI3';
 656 | $post['subtitle'] = 'PHP';
 657 | $post->save();
 658 | 
 659 | // Get attributes
 660 | $post = $this->Posts_model->findOne(1);
 661 | $title = $post->title;
 662 | $subtitle = $post['subtitle'];
 663 | ```
 664 | 
 665 | ### Relationships
 666 | 
 667 | Database tables are often related to one another. For example, a blog post may have many comments, or an order could be related to the user who placed it. This library makes managing and working with these relationships easy, and supports different types of relationships:
 668 | 
 669 | - [One To One](#hasone)
 670 | - [One To Many](#hasmany)
 671 | 
 672 | To work with relational data using Active Record, you first need to declare relations in models. The task is as simple as declaring a `relation method` for every interested relation, like the following,
 673 | 
 674 | ```php
 675 | class CustomersModel extends yidas\Model
 676 | {
 677 |     // ...
 678 | 
 679 |     public function orders()
 680 |     {
 681 |         return $this->hasMany('OrdersModel', ['customer_id' => 'id']);
 682 |     }
 683 | }
 684 | ```
 685 | 
 686 | Once the relationship is defined, we may retrieve the related record using dynamic properties. Dynamic properties allow you to access relationship methods as if they were properties defined on the model:
 687 | 
 688 | ```php
 689 | $orders = $this->CustomersModel->findOne(1)->orders;
 690 | ```
 691 | 
 692 | > The dynamic properties' names are same as methods' names, like [Laravel Eloquent](https://laravel.com/docs/5.7/eloquent-relationships)
 693 | 
 694 | For **Querying Relations**, You may query the `orders` relationship and add additional constraints with CI Query Builder to the relationship like so:
 695 | 
 696 | ```php
 697 | $customer = $this->CustomersModel->findOne(1)
 698 | 
 699 | $orders = $customer->orders()->where('active', 1)->get()->result_array();
 700 | ```
 701 | 
 702 | ### Methods
 703 | 
 704 | #### `findOne()`
 705 | 
 706 | Return a single active record model instance by a primary key or an array of column values.
 707 | 
 708 | ```php
 709 | public object findOne(array $condition=[])
 710 | ```
 711 | 
 712 | *Example:*
 713 | ```php
 714 | // Find a single active record whose primary key value is 10
 715 | $activeRecord = $this->Model->findOne(10);
 716 | 
 717 | // Find the first active record whose type is 'A' and whose status is 1
 718 | $activeRecord = $this->Model->findOne(['type' => 'A', 'status' => 1]);
 719 | 
 720 | // Query builder ORM usage
 721 | $this->Model->find()->where('id', 10);
 722 | $activeRecord = $this->Model->findOne();
 723 | ```
 724 | 
 725 | #### `findAll()`
 726 | 
 727 | Returns a list of active record models that match the specified primary key value(s) or a set of column values.
 728 | 
 729 | ```php
 730 | public array findAll(array $condition=[], integer|array $limit=null)
 731 | ```
 732 | 
 733 | *Example:*
 734 | ```php
 735 | // Find the active records whose primary key value is 10, 11 or 12.
 736 | $activeRecords = $this->Model->findAll([10, 11, 12]);
 737 | 
 738 | // Find the active recordd whose type is 'A' and whose status is 1
 739 | $activeRecords = $this->Model->findAll(['type' => 'A', 'status' => 1]);
 740 | 
 741 | // Query builder ORM usage
 742 | $this->Model->find()->where_in('id', [10, 11, 12]);
 743 | $activeRecords = $this->Model->findAll();
 744 | 
 745 | // Print all properties for each active record from array
 746 | foreach ($activeRecords as $activeRecord) {
 747 |     print_r($activeRecord->toArray());
 748 | }
 749 | ```
 750 | 
 751 | *Example of limit:*
 752 | ```php
 753 | // LIMIT 10
 754 | $activeRecords = $this->Model->findAll([], 10);
 755 | 
 756 | // OFFSET 50, LIMIT 10
 757 | $activeRecords = $this->Model->findAll([], [50, 10]);
 758 | ```
 759 | 
 760 | #### `save()`
 761 | 
 762 | Active Record (ORM) save for insert or update
 763 | 
 764 | ```php
 765 | public boolean save(boolean $runValidation=true)
 766 | ```
 767 | 
 768 | #### `beforeSave()`
 769 | 
 770 | This method is called at the beginning of inserting or updating a active record
 771 | 
 772 | 
 773 | ```php
 774 | public boolean beforeSave(boolean $insert)
 775 | ```
 776 | 
 777 | *Example:*
 778 | ```
 779 | public function beforeSave($insert)
 780 | {
 781 |     if (!parent::beforeSave($insert)) {
 782 |         return false;
 783 |     }
 784 | 
 785 |     // ...custom code here...
 786 |     return true;
 787 | }
 788 | ```
 789 | 
 790 | #### `afterSave()`
 791 | 
 792 | This method is called at the end of inserting or updating a active record
 793 | 
 794 | 
 795 | ```php
 796 | public boolean beforeSave(boolean $insert, array $changedAttributes)
 797 | ```
 798 | 
 799 | #### `hasOne()`
 800 | 
 801 | Declares a has-one relation
 802 | 
 803 | 
 804 | ```php
 805 | public CI_DB_query_builder hasOne(string $modelName, string $foreignKey=null, string $localKey=null)
 806 | ```
 807 | 
 808 | *Example:*
 809 | ```php
 810 | class OrdersModel extends yidas\Model
 811 | {
 812 |     // ...
 813 |     
 814 |     public function customer()
 815 |     {
 816 |         return $this->hasOne('CustomersModel', 'id', 'customer_id');
 817 |     }
 818 | }
 819 | ```
 820 | *Accessing Relational Data:*
 821 | ```php
 822 | $this->load->model('OrdersModel');
 823 | // SELECT * FROM `orders` WHERE `id` = 321
 824 | $order = $this->OrdersModel->findOne(321);
 825 | 
 826 | // SELECT * FROM `customers` WHERE `customer_id` = 321
 827 | // $customer is a Customers active record
 828 | $customer = $order->customer;
 829 | ```
 830 | 
 831 | #### `hasMany()`
 832 | 
 833 | Declares a has-many relation
 834 | 
 835 | 
 836 | ```php
 837 | public CI_DB_query_builder hasMany(string $modelName, string $foreignKey=null, string $localKey=null)
 838 | ```
 839 | 
 840 | *Example:*
 841 | ```php
 842 | class CustomersModel extends yidas\Model
 843 | {
 844 |     // ...
 845 |     
 846 |     public function orders()
 847 |     {
 848 |         return $this->hasMany('OrdersModel', 'customer_id', 'id');
 849 |     }
 850 | }
 851 | ```
 852 | *Accessing Relational Data:*
 853 | ```php
 854 | $this->load->model('CustomersModel');
 855 | // SELECT * FROM `customers` WHERE `id` = 123
 856 | $customer = $this->CustomersModel->findOne(123);
 857 | 
 858 | // SELECT * FROM `order` WHERE `customer_id` = 123
 859 | // $orders is an array of Orders active records
 860 | $orders = $customer->orders;
 861 | ```
 862 | 
 863 | #### `toArray()`
 864 | 
 865 | Active Record transform to array record
 866 | 
 867 | ```php
 868 | public array toArray()
 869 | ```
 870 | 
 871 | *Example:*
 872 | ```
 873 | if ($activeRecord)
 874 |     $record = $activeRecord->toArray();
 875 | ```
 876 | 
 877 | > It's recommended to use find() with CI builder instead of using ORM and turning it to array.
 878 | 
 879 | ---
 880 | 
 881 | SOFT DELETED
 882 | ------------
 883 | 
 884 | In addition to actually removing records from your database, This Model can also "soft delete" models. When models are soft deleted, they are not actually removed from your database. Instead, a `deleted_at` attribute could be set on the model and inserted into the database.
 885 | 
 886 | ### Configuration
 887 | 
 888 | You could enable SOFT DELETED feature by giving field name to `SOFT_DELETED`:
 889 | 
 890 | ```php
 891 | class My_model extends yidas\Model
 892 | {
 893 |     const SOFT_DELETED = 'is_deleted';
 894 | }
 895 | ```
 896 | 
 897 | While `SOFT_DELETED` is enabled, you could set `$softDeletedFalseValue` and `$softDeletedTrueValue` for fitting table schema. Futher, you may set `DELETED_AT` with column name for Timestapes feature, or disabled by setting to `NULL` by default:
 898 | 
 899 | ```php
 900 | class My_model extends yidas\Model
 901 | {
 902 |     const SOFT_DELETED = 'is_deleted';
 903 |     
 904 |     // The actived value for SOFT_DELETED
 905 |     protected $softDeletedFalseValue = '0';
 906 |     
 907 |     // The deleted value for SOFT_DELETED
 908 |     protected $softDeletedTrueValue = '1';
 909 | 
 910 |     const DELETED_AT = 'deleted_at';
 911 | }
 912 | ```
 913 | 
 914 | If you need to disabled SOFT DELETED feature for specified model, you may set `SOFT_DELETED` to `false`, which would disable any SOFT DELETED functions including `DELETED_AT` feature:
 915 | 
 916 | ```php
 917 | // class My_model extends yidas\Model
 918 | class Log_model extends My_model
 919 | {
 920 |     const SOFT_DELETED = false;
 921 | }
 922 | ```
 923 | 
 924 | ### Methods
 925 | 
 926 | #### `forceDelete()`
 927 | 
 928 | Force Delete the selected record(s) with Timestamps feature into the associated database table.
 929 | 
 930 | ```php
 931 | public boolean forceDelete($condition=null)
 932 | ```
 933 | 
 934 | *Example:*
 935 | ```php
 936 | $result = $this->Model->forceDelete(123)
 937 | ```
 938 | 
 939 | ```php
 940 | // Query builder ORM usage
 941 | $this->Model->find()->where('id', 123);
 942 | $result = $this->Model->forceDelete();
 943 | ```
 944 | 
 945 | #### `restore()`
 946 | 
 947 | Restore SOFT_DELETED field value to the selected record(s) into the associated database table.
 948 | 
 949 | ```php
 950 | public boolean restore($condition=null)
 951 | ```
 952 | 
 953 | *Example:*
 954 | ```php
 955 | $result = $this->Model->restore(123)
 956 | ```
 957 | 
 958 | ```php
 959 | // Query builder ORM usage
 960 | $this->Model->withTrashed()->find()->where('id', 123);
 961 | $this->Model->restore();
 962 | ```
 963 | 
 964 | #### `withTrashed()`
 965 | 
 966 | Without [SOFT DELETED](#soft-deleted) query conditions for next `find()`
 967 | 
 968 | ```php
 969 | public self withTrashed()
 970 | ```
 971 | 
 972 | *Example:*
 973 | ```php
 974 | $this->Model->withTrashed()->find();
 975 | ```
 976 | 
 977 | 
 978 | ---
 979 | 
 980 | QUERY SCOPES
 981 | ------------
 982 | 
 983 | Query scopes allow you to add constraints to all queries for a given model. Writing your own global scopes can provide a convenient, easy way to make sure every query for a given model receives certain constraints. The [SOFT DELETED](#soft-deleted) scope is a own scope which is not includes in global scope.
 984 | 
 985 | ### Configuration
 986 | 
 987 | You could override `_globalScopes` method to define your constraints:
 988 | 
 989 | ```php
 990 | class My_model extends yidas\Model
 991 | {
 992 |     protected $userAttribute = 'uid';
 993 |     
 994 |     /**
 995 |      * Override _globalScopes with User validation
 996 |      */
 997 |     protected function _globalScopes()
 998 |     {
 999 |         $this->db->where(
1000 |             $this->_field($this->userAttribute), 
1001 |             $this->config->item('user_id')
1002 |             );
1003 |         return parent::_globalScopes();
1004 |     }
1005 | ```
1006 | 
1007 | After overriding that, the `My_model` will constrain that scope in every query from `find()`, unless you remove the query scope before a find query likes `withoutGlobalScopes()`.
1008 | 
1009 | ### Methods
1010 | 
1011 | #### `withoutGlobalScopes()`
1012 | 
1013 | Without Global Scopes query conditions for next find()
1014 | 
1015 | ```php
1016 | public self withoutGlobalScopes()
1017 | ```
1018 | 
1019 | *Example:*
1020 | ```php
1021 | $this->Model->withoutGlobalScopes()->find();
1022 | ```
1023 | 
1024 | #### `withAll()`
1025 | 
1026 | Without all query conditions ([SOFT DELETED](#soft-deleted) & [QUERY SCOPES](#query-scope)) for next `find()`
1027 | 
1028 | That is, with all data set of Models for next `find()`
1029 | 
1030 | ```php
1031 | public self withAll()
1032 | ```
1033 | 
1034 | *Example:*
1035 | ```php
1036 | $this->Model->withAll()->find();
1037 | ```
1038 | ---
1039 | 
1040 | VALIDATION
1041 | ----------
1042 | 
1043 | As a rule of thumb, you should never trust the data received from end users and should always validate it before putting it to good use.
1044 | 
1045 | The ORM Model validation integrates [CodeIgniter Form Validation](https://www.codeigniter.com/userguide3/libraries/form_validation.html) that provides consistent and smooth way to deal with model data validation.
1046 | 
1047 | ### Validating Input
1048 | 
1049 | Given a model populated with user inputs, you can validate the inputs by calling the `validate()` method. The method will return a boolean value indicating whether the validation succeeded or not. If not, you may get the error messages from `getErrors()` method.
1050 | 
1051 | #### `validate()`
1052 | 
1053 | Performs the data validation with filters
1054 | 
1055 | > ORM only performs validation for assigned properties.
1056 | 
1057 | ```php
1058 | public boolean validate($data=[], $returnData=false)
1059 | ```
1060 | 
1061 | *Exmaple:*
1062 | 
1063 | ```php
1064 | $this->load->model('PostsModel');
1065 | 
1066 | if ($this->PostsModel->validate($inputData)) {
1067 |     // all inputs are valid
1068 | } else {
1069 |     // validation failed: $errors is an array containing error messages
1070 |     $errors = $this->PostsModel->getErrors();
1071 | }
1072 | ```
1073 | 
1074 | > The methods of `yidas\Model` for modifying such as `insert()` and `update()` will also perform validation. You can turn off `$runValidation` parameter of methods if you ensure that the input data has been validated.
1075 | 
1076 | *Exmaple of ORM Model:*
1077 | 
1078 | ```php
1079 | $this->load->model('PostsModel');
1080 | $post = new PostsModel;
1081 | $post->title = '';
1082 | // ORM assigned or modified attributes will be validated by calling `validate()` without parameters
1083 | if ($post->validate()) {
1084 |     // Already performing `validate()` so that turn false for $runValidation
1085 |     $result = $post->save(false);
1086 | } else {
1087 |     // validation failed: $errors is an array containing error messages
1088 |     $errors = post->getErrors();
1089 | }
1090 | ```
1091 | 
1092 | > A ORM model's properties will be changed by filter after performing validation. If you have previously called `validate()`.
1093 | You can turn off `$runValidation` of `save()` for saving without repeated validation.
1094 | 
1095 | ### getErrors()
1096 | 
1097 | Validation - Get error data referenced by last failed Validation
1098 | 
1099 | ```php
1100 | public array getErrors()
1101 | ```
1102 | 
1103 | ### Declaring Rules
1104 | 
1105 | To make `validate()` really work, you should declare validation rules for the attributes you plan to validate. This should be done by overriding the `rules()` method with returning [CodeIgniter Rules](https://www.codeigniter.com/userguide3/libraries/form_validation.html#setting-rules-using-an-array).
1106 | 
1107 | #### `rules()`
1108 | 
1109 | Returns the validation rules for attributes.
1110 | 
1111 | ```php
1112 | public array rules()
1113 | ```
1114 | 
1115 | *Example:*
1116 | 
1117 | ```php
1118 | class PostsModel extends yidas\Model
1119 | {
1120 |     protected $table = "posts";
1121 |     
1122 |     /**
1123 |      * Override rules function with validation rules setting
1124 |      */
1125 |     public function rules()
1126 |     {
1127 |         return [
1128 |             [
1129 |                 'field' => 'title',
1130 |                 'rules' => 'required|min_length[3]',
1131 |             ],
1132 |         ];
1133 |     }
1134 | }
1135 | ```
1136 | 
1137 | > The returning array format could refer [CodeIgniter - Setting Rules Using an Array](https://www.codeigniter.com/userguide3/libraries/form_validation.html#setting-rules-using-an-array), and the rules pattern could refer [CodeIgniter - Rule Reference](https://www.codeigniter.com/userguide3/libraries/form_validation.html#rule-reference)
1138 | 
1139 | #### Error Message with Language
1140 | 
1141 | When you are dealing with i18n issue of validation's error message, you can integrate [CodeIgniter language class](https://www.codeigniter.com/userguide3/libraries/language.html) into rules. The following sample code is available for you to implement:
1142 | 
1143 | ```php
1144 | public function rules()
1145 | {
1146 |     /**
1147 |      * Set CodeIgniter language
1148 |      * @see https://www.codeigniter.com/userguide3/libraries/language.html
1149 |      */
1150 |     $this->lang->load('error_messages', 'en-US');
1151 | 
1152 |     return [
1153 |         [
1154 |             'field' => 'title',
1155 |             'rules' => 'required|min_length[3]',
1156 |             'errors' => [
1157 |                 'required' => $this->lang->line('required'),
1158 |                 'min_length' => $this->lang->line('min_length'),
1159 |             ],
1160 |         ],
1161 |     ];
1162 | }
1163 | ```
1164 | 
1165 | In above case, the language file could be `application/language/en-US/error_messages_lang.php`:
1166 | 
1167 | ```php
1168 | $lang['required'] = '`%s` is required';
1169 | $lang['min_length'] = '`%s` requires at least %d letters';
1170 | ```
1171 | 
1172 | After that, the `getErrors()` could returns field error messages with current language.
1173 | 
1174 | ### Filters
1175 | 
1176 | User inputs often need to be filtered or preprocessed. For example, you may want to trim the spaces around the username input. You may declare filter rules in `filter()` method to achieve this goal.
1177 | 
1178 | > In model's `validate()` process, the `filters()` will be performed before [`rules()`](#declaring-rules), which means the input data validated by [`rules()`](#declaring-rules) is already be filtered.
1179 | 
1180 | To enable filters for `validate()`, you should declare filters for the attributes you plan to perform. This should be done by overriding the `filters()` method.
1181 | 
1182 | #### `filters()`
1183 | 
1184 | Returns the filter rules for validation.
1185 | 
1186 | ```php
1187 | public array filters()
1188 | ```
1189 | 
1190 | *Example:*
1191 | 
1192 | ```php
1193 | public function filters()
1194 | {
1195 |     return [
1196 |         [['title', 'name'], 'trim'],    // Perform `trim()` for title & name input data
1197 |         [['title'], 'static::method'],  // Perform `public static function method($value)` in this model
1198 |         [['name'], function($value) {   // Perform defined anonymous function. 'value' => '[Filtered]value'
1199 |             return "[Filtered]" . $value;
1200 |         }],
1201 |         [['content'], [$this->security, 'xss_clean']], // Perform CodeIgniter XSS Filtering for content input data
1202 |     ];
1203 | }
1204 | ```
1205 | 
1206 | > The filters format: `[[['attr1','attr2'], callable],]`
1207 | 
1208 | 
1209 | ---
1210 | 
1211 | READ & WRITE CONNECTIONS
1212 | ------------------------
1213 | 
1214 | Sometimes you may wish to use one database connection for `SELECT` statements, and another for `INSERT`, `UPDATE`, and `DELETE` statements. This Model implements Replication and Read-Write Splitting, makes database connections will always be used while using Model usages.
1215 | 
1216 | ### Configuration
1217 | 
1218 | Read & Write Connections could be set in the model which extends `yidas\Model`, you could defind the read & write databases in extended `My_model` for every models.
1219 | 
1220 | There are three types to set read & write databases:
1221 | 
1222 | #### Codeigniter DB Connection
1223 | 
1224 | It recommends to previously prepare CI DB connections, you could assign to attributes directly in construct section before parent's constrcut: 
1225 | 
1226 | ```php
1227 | class My_model extends yidas\Model
1228 | {
1229 |     function __construct()
1230 |     {
1231 |         $this->database = $this->db;
1232 |         
1233 |         $this->databaseRead = $this->dbr;
1234 |         
1235 |         parent::__construct();
1236 |     }
1237 | }
1238 | ```
1239 | 
1240 | > If you already have `$this->db`, it would be the default setting for both connection.
1241 | 
1242 | > This setting way supports [Reconnection](#reconnection).
1243 | 
1244 | #### Codeigniter Database Key
1245 | 
1246 | You could set the database key refered from `\application\config\database.php` into model attributes of `database` & `databaseRead`, the setting connections would be created automatically:
1247 | 
1248 | ```php
1249 | class My_model extends yidas\Model
1250 | {
1251 |     protected $database = 'default';
1252 |     
1253 |     protected $databaseRead = 'slave';
1254 | }
1255 | ```
1256 | 
1257 | > This method supports cache mechanism for DB connections, each model could define its own connections but share the same connection by key.
1258 | 
1259 | #### Codeigniter Database Config Array
1260 | 
1261 | This way is used for the specified model related to the one time connected database in a request cycle, which would create a new connection per each model: 
1262 | 
1263 | ```php
1264 | class My_model extends yidas\Model
1265 | {
1266 |     protected $databaseRead = [
1267 |         'dsn'   => '',
1268 |         'hostname' => 'specified_db_host',
1269 |         // Database Configuration...
1270 |         ];
1271 | }
1272 | ```
1273 | 
1274 | ### Load Balancing for Databases
1275 | 
1276 | In above case, you could set multiple databases and implement random selected connection for Read or Write Databases.
1277 | 
1278 | For example, configuring read databases in `application/config/database`:
1279 | 
1280 | ```php
1281 | $slaveHosts = ['192.168.1.2', '192.168.1.3'];
1282 | 
1283 | $db['slave']['hostname'] = $slaveHosts[mt_rand(0, count($slaveHosts) - 1)];
1284 | ```
1285 | 
1286 | After that, you could use database key `slave` to load or assign it to attribute:
1287 | 
1288 | ```php
1289 | class My_model extends yidas\Model
1290 | {
1291 |     protected $databaseRead = 'slave';
1292 | }
1293 | ```
1294 | 
1295 | ### Reconnection
1296 | 
1297 | If you want to reconnect database for reestablishing the connection in Codeigniter 3, for `$this->db` example:
1298 | 
1299 | ```php
1300 | $this->db->close();
1301 | $this->db->initialize();
1302 | ```
1303 | 
1304 | The model connections with [Codeigniter DB Connection](#codeigniter-db-connection) setting could be reset after reset the referring database connection.
1305 | 
1306 | > Do NOT use [`reconnect()`](https://www.codeigniter.com/userguide3/database/db_driver_reference.html#CI_DB_driver::reconnect) which is a useless method. 
1307 | 
1308 | ---
1309 | 
1310 | PESSIMISTIC LOCKING
1311 | -------------------
1312 | 
1313 | The Model also includes a few functions to help you do "pessimistic locking" on your `select` statements. To run the statement with a "shared lock", you may use the `sharedLock` method to get a query. A shared lock prevents the selected rows from being modified until your transaction commits:
1314 | 
1315 | ```php
1316 | $this->Model->find()->where('id', 123);
1317 | $result = $this->Model->sharedLock()->row_array();
1318 | ```
1319 | 
1320 | Alternatively, you may use the `lockForUpdate` method. A "for update" lock prevents the rows from being modified or from being selected with another shared lock:
1321 | 
1322 | ```php
1323 | $this->Model->find()->where('id', 123);
1324 | $result = $this->Model->lockForUpdate()->row_array();
1325 | ```
1326 | 
1327 | ### Example Code
1328 | 
1329 | This transaction block will lock selected rows for next same selected rows with `FOR UPDATE` lock:
1330 | 
1331 | ```php
1332 | $this->Model->getDB()->trans_start();
1333 | $this->Model->find()->where('id', 123)
1334 | $result = $this->Model->lockForUpdate()->row_array();
1335 | $this->Model->getDB()->trans_complete(); 
1336 | ```
1337 | 
1338 | ---
1339 | 
1340 | HELPERS
1341 | -------
1342 | 
1343 | The model provides several helper methods:
1344 | 
1345 | #### `indexBy()`
1346 | 
1347 | Index by Key
1348 | 
1349 | ```php
1350 | public array indexBy(array & $array, Integer $key=null, Boolean $obj2Array=false)
1351 | ```
1352 | 
1353 | *Example:*
1354 | ```php
1355 | $records = $this->Model->findAll();
1356 | $this->Model->indexBy($records, 'sn');
1357 | 
1358 | // Result example of $records:
1359 | [
1360 |     7 => ['sn'=>7, title=>'Foo'],
1361 |     13 => ['sn'=>13, title=>'Bar']
1362 | ]
1363 | ```
1364 | 
1365 | 
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
 1 | {
 2 |     "name": "yidas/codeigniter-model",
 3 |     "description": "CodeIgniter 3 ORM Base Model pattern with My_model example",
 4 |     "keywords": ["codeIgniter", "model", "base model", "my_model"],
 5 |     "homepage": "https://github.com/yidas/codeigniter-model",
 6 |     "type": "library",
 7 |     "license": "MIT",
 8 |     "support": {
 9 |         "issues": "https://github.com/yidas/codeigniter-model/issues",
10 |         "source": "https://github.com/yidas/codeigniter-model"
11 |     },
12 |     "minimum-stability": "stable",
13 |     "require": {
14 |         "php": ">=5.4.0"
15 |     },
16 |     "autoload": {
17 |         "classmap": ["src/"]
18 |     }
19 | }
20 | 
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
 1 | Example of My_model
 2 | ===================
 3 | 
 4 | The best practice to use `BaseModel` is using `My_model` to extend for every models, you could refer the document [Use My_model to Extend BaseModel for every Models](https://github.com/yidas/codeigniter-model#use-my_model-to-extend-base-model-for-every-models) for building the structure in your Codeigniter application.
 5 | 
 6 | - [User ACL](userACL)  
 7 |   For RBAC structure.
 8 | 
 9 | - [Request Log](requestLog)  
10 |   For Log Model implement with IP and User-Agent concern.
11 | 
--------------------------------------------------------------------------------
/example/requestLog/My_model.php:
--------------------------------------------------------------------------------
 1 | 
 9 |  * @version  2.0.0
10 |  * @see      https://github.com/yidas/codeigniter-model/tree/master/example
11 |  * @since    \yidas\Mdoel 2.0.0
12 |  * @see      https://github.com/yidas/codeigniter-model
13 |  */
14 | class My_model extends yidas\Model
15 | {
16 |     /* Configuration by Inheriting */
17 |     
18 |     // Fill up with your DB key of Slave Databases if needed
19 |     protected $databaseRead = false;
20 | 
21 |     // The regular PK Key in App
22 |     protected $primaryKey = 'id';
23 | 
24 |     // Mainstream creating field name
25 |     const CREATED_AT = 'created_at';
26 |     
27 |     // Log has no updating
28 |     const UPDATED_AT = null;
29 | 
30 |     protected $timestamps = true;
31 | 
32 |     // Use unixtime for saving datetime
33 |     protected $dateFormat = 'unixtime';
34 | 
35 |     // Record status for checking is deleted or not
36 |     const SOFT_DELETED = false;
37 | 
38 |     /* Application Features */
39 | 
40 |     /**
41 |      * @var string Field for IP
42 |      */
43 |     public $createdIpAttribute = 'ip';
44 |     
45 |     /**
46 |      * @var string Field for User Agent
47 |      */
48 |     public $createdUserAgentAttribute = '';
49 | 
50 |     /**
51 |      * @var string Field for Request URI
52 |      */
53 |     public $createdRequestUriAttribute = '';
54 |     
55 |     /**
56 |      * Request Headers based on $_SERVER
57 |      * 
58 |      * @var string Header => Field
59 |      * @example
60 |      *  ['HTTP_AUTHORIZATION' => 'header_auth']
61 |      */
62 |     public $createdHeaderAttributes = [];
63 | 
64 |     /**
65 |      * Override _attrEventBeforeInsert()
66 |      */
67 |     protected function _attrEventBeforeInsert(&$attributes)
68 |     {
69 |         // Auto IP
70 |         if ($this->createdIpAttribute && !isset($attributes[$this->createdIpAttribute])) {
71 |             $attributes[$this->createdIpAttribute] = $this->input->ip_address();
72 |         }
73 |         // Auto User Agent
74 |         if ($this->createdUserAgentAttribute && !isset($attributes[$this->createdUserAgentAttribute])) {
75 |             $attributes[$this->createdUserAgentAttribute] = $this->input->user_agent();
76 |         }
77 |         // Auto Request URI (`$this->uri->uri_string()` couldn't include QUERY_STRING)
78 |         if ($this->createdRequestUriAttribute && !isset($attributes[$this->createdRequestUriAttribute])) {
79 |             $attributes[$this->createdRequestUriAttribute] = isset($_SERVER['REQUEST_URI']) 
80 |                 ? $_SERVER['REQUEST_URI'] : '';
81 |         }
82 |         // Auto Hedaers
83 |         foreach ((array)$this->createdHeaderAttributes as $header => $field) {
84 |             if ($field && !isset($attributes[$field])) {
85 |                 $attributes[$field] = isset($_SERVER[$header]) 
86 |                     ? $_SERVER[$header] : '';
87 |             }
88 |         }
89 |         
90 |         return parent::_attrEventBeforeInsert($attributes);
91 |     }
92 | }
93 | 
--------------------------------------------------------------------------------
/example/requestLog/README.md:
--------------------------------------------------------------------------------
 1 | Log My_model
 2 | ============
 3 | 
 4 | Use for log table without ACL concern.
 5 | 
 6 | ---
 7 | 
 8 | TABLE SCHEMA
 9 | ------------
10 | 
11 | ### MySQL
12 | 
13 | ```sql
14 | CREATE TABLE `table` (
15 |   `id` bigint(20) UNSIGNED NOT NULL,
16 |   `ip` char(15) DEFAULT NULL COMMENT 'IP header',
17 |   `user_agent` varchar(255) DEFAULT NULL COMMENT 'User-Agent header',
18 |   `created_at` datetime NOT NULL
19 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
20 | 
21 | ALTER TABLE `table`
22 |   ADD PRIMARY KEY (`id`);
23 | 
24 | ALTER TABLE `table`
25 |   MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT;COMMIT;
26 | ```
27 | 
28 | 
--------------------------------------------------------------------------------
/example/userACL/My_model.php:
--------------------------------------------------------------------------------
  1 | 
 15 |  * @version  2.0.0
 16 |  * @see      https://github.com/yidas/codeigniter-model/tree/master/example
 17 |  * @since    \yidas\Mdoel 2.0.0
 18 |  * @see      https://github.com/yidas/codeigniter-model
 19 |  */
 20 | class My_model extends yidas\Model
 21 | {
 22 |     /* Configuration by Inheriting */
 23 |     
 24 |     // Fill up with your DB key of Slave Databases if needed
 25 |     protected $databaseRead = false;
 26 | 
 27 |     // The regular PK Key in App
 28 |     protected $primaryKey = 'id';
 29 | 
 30 |     protected $timestamps = true;
 31 | 
 32 |     // Mainstream creating field name
 33 |     const CREATED_AT = 'created_at';
 34 | 
 35 |     // Mainstream updating field name
 36 |     const UPDATED_AT = 'updated_at';
 37 | 
 38 |     // Use unixtime for saving datetime
 39 |     protected $dateFormat = 'unixtime';
 40 | 
 41 |     // Record status for checking is deleted or not
 42 |     const SOFT_DELETED = 'is_deleted';
 43 | 
 44 |     // 0: actived, 1: deleted
 45 |     protected $recordDeletedFalseValue = '1';
 46 | 
 47 |     protected $recordDeletedTrueValue = '0';
 48 | 
 49 |     const DELETED_AT = 'deleted_at';
 50 | 
 51 | 
 52 |     /* Application Features */
 53 | 
 54 |     /**
 55 |      * @var string Auto Field for user SN
 56 |      */
 57 |     protected $userAttribute = 'user_id';
 58 | 
 59 |     /**
 60 |      * @var string Auto Field for company SN
 61 |      */
 62 |     protected $companyAttribute = 'company_id';
 63 | 
 64 |     /**
 65 |      * @var string Field for created user
 66 |      */
 67 |     protected $createdUserAttribute = 'created_by';
 68 | 
 69 |     /**
 70 |      * @var string Field for updated user
 71 |      */
 72 |     protected $updatedUserAttribute = 'updated_by';
 73 | 
 74 |     /**
 75 |      * @var string Field for deleted user
 76 |      */
 77 |     protected $deletedUserAttribute = 'deleted_by';
 78 | 
 79 |     function __construct()
 80 |     {
 81 |         parent::__construct();
 82 | 
 83 |         // Load your own user library for companyID and userID data 
 84 |         $this->load->library("user");
 85 |     }
 86 | 
 87 |     /**
 88 |      * Override _globalScopes with User & Company validation
 89 |      */
 90 |     protected function _globalScopes()
 91 |     {
 92 |         if ($this->companyAttribute) {
 93 |             
 94 |             $this->getBuilder()->where(
 95 |                 $this->_field($this->companyAttribute), 
 96 |                 $this->user->getCompanyID();
 97 |                 );
 98 |         }
 99 |         
100 |         if ($this->userAttribute) {
101 |             
102 |             $this->getBuilder()->where(
103 |                 $this->_field($this->userAttribute), 
104 |                 $this->user->getID();
105 |                 );
106 |         }
107 |         return parent::_globalScopes();
108 |     }
109 |     /**
110 |      * Override _attrEventBeforeInsert()
111 |      */
112 |     protected function _attrEventBeforeInsert(&$attributes)
113 |     {
114 |         // Auto Company
115 |         if ($this->companyAttribute && !isset($attributes[$this->companyAttribute])) {
116 |             
117 |             $attributes[$this->companyAttribute] = $this->user->getCompanyID();;
118 |         }
119 |         // Auto User
120 |         if ($this->userAttribute && !isset($attributes[$this->userAttribute])) {
121 |             
122 |             $attributes[$this->userAttribute] = $this->user->getID();;
123 |         }
124 |         // Auto created_by
125 |         if ($this->createdUserAttribute && !isset($attributes[$this->createdUserAttribute])) {
126 |             $attributes[$this->createdUserAttribute] = $this->user->getID();
127 |         }
128 |         
129 |         return parent::_attrEventBeforeInsert($attributes);
130 |     }
131 |     /**
132 |      * Override _attrEventBeforeUpdate()
133 |      */
134 |     public function _attrEventBeforeUpdate(&$attributes)
135 |     {
136 |         // Auto updated_by
137 |         if ($this->updatedUserAttribute && !isset($attributes[$this->updatedUserAttribute])) {
138 |             $attributes[$this->updatedUserAttribute] = $this->user->getID();
139 |         }
140 |         return parent::_attrEventBeforeUpdate($attributes);
141 |     }
142 |     /**
143 |      * Override _attrEventBeforeDelete()
144 |      */
145 |     public function _attrEventBeforeDelete(&$attributes)
146 |     {
147 |         // Auto deleted_by
148 |         if ($this->deletedUserAttribute && !isset($attributes[$this->deletedUserAttribute])) {
149 |             $attributes[$this->deletedUserAttribute] = $this->user->getID();
150 |         }
151 |         return parent::_attrEventBeforeDelete($attributes);
152 |     }
153 | }
154 | 
155 | 
156 | 
--------------------------------------------------------------------------------
/example/userACL/README.md:
--------------------------------------------------------------------------------
  1 | User ACL My_model
  2 | =================
  3 | 
  4 | This example My_model assumes that a user is belong to a company, so each data row is belong to a user with that company. The Model basic funcitons overrided BaseModel with user and company verification to implement the protection. 
  5 | 
  6 | >Based on BaseModel, My_model is customized for your web application with schema such as primary key and column names for behavior setting. Futher, all of your model may need access features, such as the verification of user ID and company ID for multiple user layers.
  7 | 
  8 | ---
  9 | 
 10 | CONFIGURATION
 11 | -------------
 12 | 
 13 | ```php
 14 | class My_model extends BaseModel
 15 | {
 16 |     /* Configuration by Inheriting */
 17 |     
 18 |     // The regular PK Key in App
 19 |     protected $primaryKey = 'id';
 20 |     // Timestamps on
 21 |     protected $timestamps = true;
 22 |     // Soft Deleted on
 23 |     const SOFT_DELETED = 'is_deleted';
 24 |     
 25 |     protected function _globalScopes()
 26 |     {
 27 |         // Global Scope...
 28 |     }
 29 |     
 30 |     protected function _attrEventBeforeInsert(&$attributes)
 31 |     {
 32 |         // Insert Behavior...
 33 |     }
 34 |     
 35 |     // Other Behaviors...
 36 | }
 37 | ```
 38 | 
 39 | 
 40 | Defining Models
 41 | ---------------
 42 | 
 43 | ### User ACL 
 44 | 
 45 | By default, `My_model` assumes that each row data in model is belong to a user, which means the Model would find and create data owned by current user. You may set `$userAttribute` to `NULL` to disable user ACL:
 46 | 
 47 | ```php
 48 | class Post_model extends My_model
 49 | {
 50 |     protected $userAttribute = false;
 51 | }
 52 | ```
 53 | 
 54 | If you need to customize the names of the user ACL column, you may set the `$userAttribute` arrtibute in your specified model:
 55 | 
 56 | ```php
 57 | class Post_model extends My_model
 58 | {
 59 |     protected $userAttribute = 'user_id_for_post';
 60 | }
 61 | ```
 62 | 
 63 | ### Company ACL 
 64 | 
 65 | By default, `My_model` assumes that each row data in model is belong to a company, which means the Model would find and create data owned by current company. You may set `$companyAttribute` to `NULL` to disable company ACL:
 66 | 
 67 | ```php
 68 | class Post_model extends My_model
 69 | {
 70 |     protected $companyAttribute = false;
 71 | }
 72 | ```
 73 | 
 74 | If you need to customize the names of the company ACL column, you may set the `$companyAttribute` arrtibute in your specified model:
 75 | 
 76 | ```php
 77 | class Post_model extends My_model
 78 | {
 79 |     protected $companyAttribute = 'company_id_for_post';
 80 | }
 81 | ```
 82 | 
 83 | > If user ACL and company ACL are both disbled, which means this model won't filter any ACL scopes.
 84 | 
 85 | ### Transaction Log
 86 | 
 87 | Likes Timestamps feature, you may need to record transaction Log for each row. By default, This example `My_model` expects `created_by` , `updated_by` and `deleted_by` columns to exist on your tables. If you do not wish to have these columns automatically managed by `My_model`, set each property on your model to `NULL`:
 88 | 
 89 | ```php
 90 | class Post_model extends My_model
 91 | {
 92 |     protected $createdUserAttribute = 'created_by';
 93 |     
 94 |     protected $updatedUserAttribute = 'updated_by';
 95 |     
 96 |     protected $deletedUserAttribute = NULL;
 97 | }
 98 | ```
 99 | 
100 | ---
101 | 
102 | TABLE SCHEMA
103 | ------------
104 | 
105 | ### MySQL
106 | 
107 | ```sql
108 | CREATE TABLE `table` (
109 |   `id` bigint(20) NOT NULL,
110 |   `company_id` bigint(20) NOT NULL
111 |   `user_id` bigint(20) NOT NULL
112 |   `created_at` datetime NOT NULL
113 |   `created_by` bigint(20) UNSIGNED NOT NULL
114 |   `updated_at` datetime NOT NULL
115 |   `updated_by` bigint(20) UNSIGNED NOT NULL
116 |   `deleted_at` datetime NOT NULL
117 |   `deleted_by` bigint(20) UNSIGNED NOT NULL
118 |   `is_deleted` enum('0','1') NOT NULL DEFAULT '0'
119 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
120 | 
121 | --
122 | -- Indexes for dumped tables
123 | --
124 | 
125 | --
126 | -- Indexes for table `table`
127 | --
128 | ALTER TABLE `table`
129 |   ADD PRIMARY KEY (`id`),
130 |   ADD KEY `company_id` (`company_id`),
131 |   ADD KEY `user_id` (`user_id`);
132 | 
133 | --
134 | -- AUTO_INCREMENT for dumped tables
135 | --
136 | 
137 | --
138 | -- AUTO_INCREMENT for table `table`
139 | --
140 | ALTER TABLE `table`
141 |   MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT;COMMIT;
142 | ```
143 | 
144 | 
--------------------------------------------------------------------------------
/src/Model.php:
--------------------------------------------------------------------------------
   1 | 
  11 |  * @version  2.19.3
  12 |  * @see      https://github.com/yidas/codeigniter-model
  13 |  */
  14 | class Model extends \CI_Model implements \ArrayAccess
  15 | {
  16 |     /**
  17 |      * Database Configuration for read-write master
  18 |      * 
  19 |      * @var object|string|array CI DB ($this->db as default), CI specific group name or CI database config array
  20 |      */
  21 |     protected $database = "";
  22 | 
  23 |     /**
  24 |      * Database Configuration for read-only slave
  25 |      * 
  26 |      * @var object|string|array CI DB ($this->db as default), CI specific group name or CI database config array
  27 |      */
  28 |     protected $databaseRead = "";
  29 |     
  30 |     /**
  31 |      * Table name
  32 |      *
  33 |      * @var string
  34 |      */
  35 |     protected $table = "";
  36 | 
  37 |     /**
  38 |      * Table alias name
  39 |      *
  40 |      * @var string
  41 |      */
  42 |     protected $alias = null;
  43 | 
  44 |     /**
  45 |      * Primary key of table
  46 |      *
  47 |      * @var string Field name of single column primary key
  48 |      */
  49 |     protected $primaryKey = 'id';
  50 | 
  51 |     /**
  52 |      * Fillable columns of table
  53 |      *
  54 |      * @var array Field names of columns
  55 |      */
  56 |     protected $fillable = [];
  57 | 
  58 |     /**
  59 |      * Indicates if the model should be timestamped.
  60 |      *
  61 |      * @var bool
  62 |      */
  63 |     protected $timestamps = true;
  64 | 
  65 |     /**
  66 |      * Date format for timestamps.
  67 |      *
  68 |      * @var string unixtime|datetime
  69 |      */
  70 |     protected $dateFormat = 'datetime';
  71 | 
  72 |     /**
  73 |      * @string Feild name for created_at, empty is disabled.
  74 |      */
  75 |     const CREATED_AT = 'created_at';
  76 | 
  77 |     /**
  78 |      * @string Feild name for updated_at, empty is disabled.
  79 |      */
  80 |     const UPDATED_AT = 'updated_at';
  81 | 
  82 |     /**
  83 |      * CREATED_AT triggers UPDATED_AT.
  84 |      *
  85 |      * @var bool
  86 |      */
  87 |     protected $createdWithUpdated = true;
  88 | 
  89 |     /**
  90 |      * @var string Feild name for SOFT_DELETED, empty is disabled.
  91 |      */
  92 |     const SOFT_DELETED = '';
  93 | 
  94 |     /**
  95 |      * The active value for SOFT_DELETED
  96 |      *
  97 |      * @var mixed
  98 |      */
  99 |     protected $softDeletedFalseValue = '0';
 100 | 
 101 |     /**
 102 |      * The deleted value for SOFT_DELETED
 103 |      *
 104 |      * @var mixed
 105 |      */
 106 |     protected $softDeletedTrueValue = '1';
 107 | 
 108 |     /**
 109 |      * This feature is actvied while having SOFT_DELETED
 110 |      *
 111 |      * @var string Feild name for deleted_at, empty is disabled.
 112 |      */
 113 |     const DELETED_AT = '';
 114 | 
 115 |     /**
 116 |      * Check property schema for write
 117 |      *
 118 |      * @var boolean
 119 |      */
 120 |     protected $propertyCheck = false;
 121 | 
 122 |     /**
 123 |      * @var array Validation errors (depends on validator driver)
 124 |      */
 125 |     protected $_errors;
 126 | 
 127 |     /**
 128 |      * @var object database connection for write
 129 |      */
 130 |     protected $_db;
 131 | 
 132 |     /**
 133 |      * @var object database connection for read (Salve)
 134 |      */
 135 |     protected $_dbr;
 136 | 
 137 |     /**
 138 |      * @var object database caches by database key for write
 139 |      */
 140 |     protected static $_dbCaches = [];
 141 |     
 142 |     /**
 143 |      * @var object database caches by database key for read (Salve)
 144 |      */
 145 |     protected static $_dbrCaches = [];
 146 | 
 147 |     /**
 148 |      * @var object ORM schema caches by model class namespace
 149 |      */
 150 |     private static $_ormCaches = [];
 151 | 
 152 |     /**
 153 |      * @var bool SOFT_DELETED one time switch
 154 |      */
 155 |     private $_withoutSoftDeletedScope = false;
 156 | 
 157 |     /**
 158 |      * @var bool Global Scope one time switch
 159 |      */
 160 |     private $_withoutGlobalScope = false;
 161 | 
 162 |     /**
 163 |      * ORM read properties
 164 |      *
 165 |      * @var array
 166 |      */
 167 |     private $_readProperties = [];
 168 | 
 169 |     /**
 170 |      * ORM write properties
 171 |      *
 172 |      * @var array
 173 |      */
 174 |     private $_writeProperties = [];
 175 | 
 176 |     /**
 177 |      * ORM self query
 178 |      *
 179 |      * @var string
 180 |      */
 181 |     private $_selfCondition = null;
 182 | 
 183 |     /**
 184 |      * Clean next find one time setting
 185 |      *
 186 |      * @var boolean
 187 |      */
 188 |     private $_cleanNextFind = false;
 189 | 
 190 |     /**
 191 |      * Relationship property caches by method name
 192 |      *
 193 |      * @var array
 194 |      */
 195 |     private $_relationshipCaches = [];
 196 | 
 197 |     /**
 198 |      * Constructor
 199 |      */
 200 |     function __construct()
 201 |     {
 202 |         /* Database Connection Setting */
 203 |         // Master
 204 |         if ($this->database) {
 205 |             if (is_object($this->database)) {
 206 |                 // CI DB Connection
 207 |                 $this->_db = $this->database;
 208 |             } 
 209 |             elseif (is_string($this->database)) {
 210 |                 // Cache Mechanism
 211 |                 if (isset(self::$_dbCaches[$this->database])) {
 212 |                     $this->_db = self::$_dbCaches[$this->database];
 213 |                 } else {
 214 |                     // CI Database Configuration
 215 |                     $this->_db = get_instance()->load->database($this->database, true);
 216 |                     self::$_dbCaches[$this->database] = $this->_db;
 217 |                 }
 218 |             }
 219 |             else {
 220 |                 // Config array for each Model
 221 |                 $this->_db = get_instance()->load->database($this->database, true);
 222 |             }
 223 |         } else {
 224 |             // CI Default DB Connection
 225 |             $this->_db = $this->_getDefaultDB();
 226 |         }
 227 |         // Slave
 228 |         if ($this->databaseRead) {
 229 |             if (is_object($this->databaseRead)) {
 230 |                 // CI DB Connection
 231 |                 $this->_dbr = $this->databaseRead;
 232 |             } 
 233 |             elseif (is_string($this->databaseRead)) {
 234 |                 // Cache Mechanism
 235 |                 if (isset(self::$_dbrCaches[$this->databaseRead])) {
 236 |                     $this->_dbr = self::$_dbrCaches[$this->databaseRead];
 237 |                 } else {
 238 |                     // CI Database Configuration
 239 |                     $this->_dbr = get_instance()->load->database($this->databaseRead, true);
 240 |                     self::$_dbrCaches[$this->databaseRead] = $this->_dbr;
 241 |                 }
 242 |             }
 243 |             else {
 244 |                 // Config array for each Model
 245 |                 $this->_dbr = get_instance()->load->database($this->databaseRead, true);
 246 |             }
 247 |         } else {
 248 |             // CI Default DB Connection
 249 |             $this->_dbr = $this->_getDefaultDB();
 250 |         }
 251 |         
 252 |         /* Table Name Guessing */
 253 |         if (!$this->table) {
 254 |             $this->table = str_replace('_model', '', strtolower(get_called_class()));
 255 |         }
 256 |     }
 257 | 
 258 |     /**
 259 |      * Get Master Database Connection
 260 |      * 
 261 |      * @return object CI &DB
 262 |      */
 263 |     public function getDatabase()
 264 |     {
 265 |         return $this->_db;
 266 |     }
 267 | 
 268 |     /**
 269 |      * Get Slave Database Connection
 270 |      * 
 271 |      * @return object CI &DB
 272 |      */
 273 |     public function getDatabaseRead()
 274 |     {
 275 |         return $this->_dbr;
 276 |     }
 277 | 
 278 |     /**
 279 |      * Alias of getDatabase()
 280 |      */
 281 |     public function getDB()
 282 |     {
 283 |         return $this->getDatabase();
 284 |     }
 285 | 
 286 |     /**
 287 |      * Alias of getDatabaseRead()
 288 |      */
 289 |     public function getDBR()
 290 |     {
 291 |         return $this->getDatabaseRead();
 292 |     }
 293 | 
 294 |     /**
 295 |      * Alias of getDatabaseRead()
 296 |      */
 297 |     public function getBuilder()
 298 |     {
 299 |         return $this->getDatabaseRead();
 300 |     }
 301 | 
 302 |     /**
 303 |      * Get table name
 304 |      *
 305 |      * @return string Table name
 306 |      */
 307 |     public function getTable()
 308 |     {
 309 |         return $this->table;
 310 |     }
 311 | 
 312 |     /**
 313 |      * Alias of getTable()
 314 |      */
 315 |     public function tableName()
 316 |     {
 317 |         return $this->getTable();
 318 |     }
 319 | 
 320 |     /**
 321 |      * Returns the filter rules for validation.
 322 |      *
 323 |      * @return array Filter rules. [[['attr1','attr2'], 'callable'],]
 324 |      */
 325 |     public function filters()
 326 |     {
 327 |         return [];
 328 |     }
 329 | 
 330 |     /**
 331 |      * Returns the validation rules for attributes.
 332 |      * 
 333 |      * @see https://www.codeigniter.com/userguide3/libraries/form_validation.html#rule-reference
 334 |      * @return array validation rules. (CodeIgniter Rule Reference)
 335 |      */
 336 |     public function rules()
 337 |     {
 338 |         return [];
 339 |     }
 340 | 
 341 |     /**
 342 |      * Performs the data validation with filters
 343 |      * 
 344 |      * ORM only performs validation for assigned properties.
 345 |      * 
 346 |      * @param array Data of attributes
 347 |      * @param boolean Return filtered data
 348 |      * @return boolean Result
 349 |      * @return mixed Data after filter ($returnData is true)
 350 |      */
 351 |     public function validate($attributes=[], $returnData=false)
 352 |     {
 353 |         // Data fetched by ORM or input
 354 |         $data = ($attributes) ? $attributes : $this->_writeProperties;
 355 |         // Filter first
 356 |         $data = $this->filter($data);
 357 |         // ORM re-assign properties
 358 |         $this->_writeProperties = (!$attributes) ? $data : $this->_writeProperties;
 359 |         // Get validation rules from function setting
 360 |         $rules = $this->rules();
 361 | 
 362 |         // The ORM update will only collect rules with corresponding modified attributes.
 363 |         if ($this->_selfCondition) {
 364 | 
 365 |             $newRules = [];
 366 |             foreach ((array) $rules as $key => $rule) {
 367 |                 if (isset($this->_writeProperties[$rule['field']])) {
 368 |                     // Add into new rules for updating
 369 |                     $newRules[] = $rule;
 370 |                 }
 371 |             }
 372 |             // Replace with mapping rules
 373 |             $rules = $newRules;
 374 |         }
 375 | 
 376 |         // Check if has rules
 377 |         if (empty($rules))
 378 |             return ($returnData) ? $data : true;
 379 | 
 380 |         // CodeIgniter form_validation doesn't work with empty array data
 381 |         if (empty($data))
 382 |             return false;
 383 | 
 384 |         // Load CodeIgniter form_validation library for yidas/model namespace, which has no effect on common one
 385 |         get_instance()->load->library('form_validation', null, 'yidas_model_form_validation');
 386 |         // Get CodeIgniter validator
 387 |         $validator = get_instance()->yidas_model_form_validation;
 388 |         $validator->reset_validation();
 389 |         $validator->set_data($data);
 390 |         $validator->set_rules($rules);
 391 |         // Run Validate
 392 |         $result = $validator->run();
 393 |         
 394 |         // Result handle
 395 |         if ($result===false) {
 396 | 
 397 |             $this->_errors = $validator->error_array();
 398 |             return false;
 399 | 
 400 |         } else {
 401 | 
 402 |             return ($returnData) ? $data : true;
 403 |         }
 404 |     }
 405 | 
 406 |     /**
 407 |      * Validation - Get error data referenced by last failed Validation
 408 |      *
 409 |      * @return array 
 410 |      */
 411 |     public function getErrors()
 412 |     {
 413 |         return $this->_errors;
 414 |     }
 415 | 
 416 |     /**
 417 |      * Validation - Reset errors
 418 |      *
 419 |      * @return boolean
 420 |      */
 421 |     public function resetErrors()
 422 |     {
 423 |         $this->_errors = null;
 424 | 
 425 |         return true;
 426 |     }
 427 | 
 428 |     /**
 429 |      * Filter process
 430 |      *
 431 |      * @param array $data Attributes
 432 |      * @return array Filtered data
 433 |      */
 434 |     public function filter($data)
 435 |     {
 436 |         // Get filter rules
 437 |         $filters = $this->filters();
 438 | 
 439 |         // Filter process with setting check
 440 |         if (!empty($filters) && is_array($filters)) {
 441 |             
 442 |             foreach ($filters as $key => $filter) { 
 443 |                 
 444 |                 if (!isset($filter[0]))
 445 |                     throw new Exception("No attributes defined in \$filters from " . get_called_class() . " (" . __CLASS__ . ")", 500);
 446 | 
 447 |                 if (!isset($filter[1]))
 448 |                     throw new Exception("No function defined in \$filters from " . get_called_class() . " (" . __CLASS__ . ")", 500);
 449 | 
 450 |                 list($attributes, $function) = $filter;
 451 | 
 452 |                 $attributes = (is_array($attributes)) ? $attributes : [$attributes];
 453 | 
 454 |                 // Filter each attribute
 455 |                 foreach ($attributes as $key => $attribute) {
 456 | 
 457 |                     if (!isset($data[$attribute]))
 458 |                         continue;
 459 |                     
 460 |                     $data[$attribute] = call_user_func($function, $data[$attribute]);
 461 |                 }
 462 |             }
 463 |         }
 464 |         
 465 |         return $data;
 466 |     }
 467 | 
 468 |     /**
 469 |      * Set table alias for next find()
 470 |      *
 471 |      * @param string Table alias name
 472 |      * @return $this
 473 |      */
 474 |     public function setAlias($alias)
 475 |     {
 476 |         $this->alias = $alias;
 477 | 
 478 |         // Turn off cleaner to prevent continuous setting 
 479 |         $this->_cleanNextFind = false;
 480 |         
 481 |         return $this;
 482 |     }
 483 | 
 484 |     /**
 485 |      * Create an existent CI Query Builder instance with Model features for query purpose.
 486 |      *
 487 |      * @param boolean $withAll withAll() switch helper
 488 |      * @return \CI_DB_query_builder CI_DB_query_builder
 489 |      * @example 
 490 |      *  $posts = $this->PostModel->find()
 491 |      *      ->where('is_public', '1')
 492 |      *      ->limit(0,25)
 493 |      *      ->order_by('id')
 494 |      *      ->get()
 495 |      *      ->result_array();
 496 |      * @example 
 497 |      *  // Without all featured conditions for next find()
 498 |      *  $posts = $this->PostModel->find(true)
 499 |      *      ->where('is_deleted', '1')
 500 |      *      ->get()
 501 |      *      ->result_array();
 502 |      *  // This is equal to withAll() method
 503 |      *  $this->PostModel->withAll()->find();
 504 |      *
 505 |      */
 506 |     public function find($withAll=false)
 507 |     {
 508 |         $instance = (isset($this)) ? $this : new static;
 509 |         
 510 |         // One time setting reset mechanism
 511 |         if ($instance->_cleanNextFind === true) {
 512 |             // Reset alias
 513 |             $instance->setAlias(null);
 514 |         } else {
 515 |             // Turn on clean for next find
 516 |             $instance->_cleanNextFind = true;
 517 |         }
 518 |         
 519 |         // Alias option for FROM
 520 |         $sqlFrom = ($instance->alias) ? "{$instance->table} AS {$instance->alias}" : $instance->table;
 521 |         
 522 |         $instance->_dbr->from($sqlFrom);
 523 | 
 524 |         // WithAll helper
 525 |         if ($withAll===true) {
 526 |             $instance->withAll();
 527 |         }
 528 | 
 529 |         // Scope condition
 530 |         $instance->_addGlobalScopeCondition();
 531 | 
 532 |         // Soft Deleted condition
 533 |         $instance->_addSoftDeletedCondition();
 534 | 
 535 |         return $instance->_dbr;
 536 |     }
 537 | 
 538 |     /**
 539 |      * Create an CI Query Builder instance without Model Filters for query purpose.
 540 |      *
 541 |      * @return \CI_DB_query_builder CI_DB_query_builder
 542 |      */
 543 |     public function forceFind()
 544 |     {
 545 |         return $this->withAll()->find();
 546 |     }
 547 | 
 548 |     /**
 549 |      * Return a single active record model instance by a primary key or an array of column values.
 550 |      *
 551 |      * @param mixed $condition Refer to _findByCondition() for the explanation of this parameter
 552 |      * @return object ActiveRecord(Model)
 553 |      * @example
 554 |      *  $post = $this->Model->findOne(123);
 555 |      * @example
 556 |      *  // Query builder ORM usage
 557 |      *  $this->Model->find()->where('id', 123);
 558 |      *  $this->Model->findOne();
 559 |      */
 560 |     public static function findOne($condition=[])
 561 |     {
 562 |         $instance = (isset($this)) ? $this : new static;
 563 |         
 564 |         $record = $instance->_findByCondition($condition)
 565 |             ->limit(1)
 566 |             ->get()->row_array();
 567 |         
 568 |         // Record check
 569 |         if (!$record) {
 570 |             return $record;
 571 |         }
 572 | 
 573 |         return $instance->createActiveRecord($record, $record[$instance->primaryKey]);
 574 |     }
 575 | 
 576 |     /**
 577 |      * Returns a list of active record models that match the specified primary key value(s) or a set of column values.
 578 |      *
 579 |      * @param mixed $condition Refer to _findByCondition() for the explanation 
 580 |      * @param integer|array $limit Limit or [offset, limit]
 581 |      * @return array Set of ActiveRecord(Model)s
 582 |      * @example
 583 |      *  $post = $this->PostModel->findAll([3,21,135]);
 584 |      * @example
 585 |      *  // Query builder ORM usage
 586 |      *  $this->Model->find()->where_in('id', [3,21,135]);
 587 |      *  $this->Model->findAll();
 588 |      */
 589 |     public static function findAll($condition=[], $limit=null)
 590 |     {
 591 |         $instance = (isset($this)) ? $this : new static;
 592 | 
 593 |         $query = $instance->_findByCondition($condition);
 594 | 
 595 |         // Limit / offset
 596 |         if ($limit) {
 597 | 
 598 |             $offset = null;
 599 |             
 600 |             if (is_array($limit) && isset($limit[1])) {
 601 |                 // Prevent list() variable effect
 602 |                 $set = $limit;
 603 |                 list($offset, $limit) = $set;
 604 |             }
 605 |             
 606 |             $query = ($limit) ? $query->limit($limit) : $query;
 607 |             $query = ($offset) ? $query->offset($offset) : $query;
 608 |         }
 609 |         
 610 |         $records = $query->get()->result_array();
 611 | 
 612 |         // Record check
 613 |         if (!$records) {
 614 |             return $records;
 615 |         }
 616 | 
 617 |         $set = [];
 618 |         // Each ActiveRecord
 619 |         foreach ((array)$records as $key => $record) {
 620 |             // Check primary key setting
 621 |             if (!isset($record[$instance->primaryKey])) {
 622 |                 throw new Exception("Model's primary key not set", 500); 
 623 |             }
 624 |             // Create an ActiveRecord into collect
 625 |             $set[] = $instance->createActiveRecord($record, $record[$instance->primaryKey]);
 626 |         }
 627 | 
 628 |         return $set;
 629 |     }
 630 | 
 631 |     /**
 632 |      * reset an CI Query Builder instance with Model.
 633 |      *
 634 |      * @return $this
 635 |      * @example 
 636 |      *  $this->Model->reset()->find();
 637 |      */
 638 |     public function reset()
 639 |     {
 640 |         // Reset query
 641 |         $this->_db->reset_query();
 642 |         $this->_dbr->reset_query();
 643 |         
 644 |         return $this;
 645 |     }
 646 | 
 647 |     /**
 648 |      * Insert a row with Timestamps feature into the associated database table using the attribute values of this record.
 649 |      * 
 650 |      * @param array $attributes
 651 |      * @param boolean $runValidation Whether to perform validation (calling validate()) before manipulate the record. 
 652 |      * @return boolean Result
 653 |      * @example
 654 |      *  $result = $this->Model->insert([
 655 |      *      'name' => 'Nick Tsai',
 656 |      *      'email' => 'myintaer@gmail.com',
 657 |      *  ]);
 658 |      */
 659 |     public function insert($attributes, $runValidation=true)
 660 |     {
 661 |         // Validation
 662 |         if ($runValidation && false===$attributes=$this->validate($attributes, true))
 663 |             return false; 
 664 |         
 665 |         $this->_attrEventBeforeInsert($attributes);
 666 | 
 667 |         if ($this->fillable) $attributes = array_intersect_key($attributes, array_flip($this->fillable)); 
 668 | 
 669 |         return $this->_db->insert($this->table, $attributes);
 670 |     }
 671 | 
 672 |     /**
 673 |      * Insert a batch of rows with Timestamps feature into the associated database table using the attribute values of this record.
 674 |      * 
 675 |      * @param array $data The rows to be batch inserted
 676 |      * @param boolean $runValidation Whether to perform validation (calling validate()) before manipulate the record. 
 677 |      * @return int Number of rows inserted or FALSE on failure
 678 |      * @example
 679 |      *  $result = $this->Model->batchInsert([
 680 |      *      ['name' => 'Nick Tsai', 'email' => 'myintaer@gmail.com'],
 681 |      *      ['name' => 'Yidas', 'email' => 'service@yidas.com']
 682 |      *  ]);
 683 |      */
 684 |     public function batchInsert($data, $runValidation=true)
 685 |     {
 686 |         foreach ($data as $key => &$attributes) {
 687 | 
 688 |             // Validation
 689 |             if ($runValidation && false===$attributes=$this->validate($attributes, true))
 690 |                 return false; 
 691 | 
 692 |             $this->_attrEventBeforeInsert($attributes);
 693 |         }
 694 | 
 695 |         return $this->_db->insert_batch($this->table, $data);
 696 |     }
 697 | 
 698 |     /**
 699 |      * Get the insert ID number when performing database inserts.
 700 |      *
 701 |      * @param string $name Name of the sequence object from which the ID should be returned.
 702 |      * @return integer Last insert ID
 703 |      */
 704 |     public function getLastInsertID($name=null)
 705 |     {
 706 |         return $this->getDB()->insert_id($name);
 707 |     }
 708 | 
 709 |     /**
 710 |      * Replace a row with Timestamps feature into the associated database table using the attribute values of this record.
 711 |      * 
 712 |      * @param array $attributes
 713 |      * @param boolean $runValidation Whether to perform validation (calling validate()) before manipulate the record. 
 714 |      * @return bool Result
 715 |      * @example
 716 |      *  $result = $this->Model->replace([
 717 |      *      'id' => 1,
 718 |      *      'name' => 'Nick Tsai',
 719 |      *      'email' => 'myintaer@gmail.com',
 720 |      *  ]);
 721 |      */
 722 |     public function replace($attributes, $runValidation=true)
 723 |     {
 724 |         // Validation
 725 |         if ($runValidation && false===$attributes=$this->validate($attributes, true))
 726 |             return false; 
 727 |         
 728 |         $this->_attrEventBeforeInsert($attributes);
 729 | 
 730 |         return $this->_db->replace($this->table, $attributes);
 731 |     }
 732 | 
 733 |     /**
 734 |      * Save the changes with Timestamps feature to the selected record(s) into the associated database table.
 735 |      * 
 736 |      * @param array $attributes
 737 |      * @param mixed $condition Refer to _findByCondition() for the explanation 
 738 |      * @param boolean $runValidation Whether to perform validation (calling validate()) before manipulate the record. 
 739 |      * @return bool Result
 740 |      *
 741 |      * @example    
 742 |      *  $this->Model->update(['status'=>'off'], 123)
 743 |      * @example
 744 |      *  // Query builder ORM usage
 745 |      *  $this->Model->find()->where('id', 123);
 746 |      *  $this->Model->update(['status'=>'off']);
 747 |      */
 748 |     public function update($attributes, $condition=NULL, $runValidation=true)
 749 |     {
 750 |         // Validation
 751 |         if ($runValidation && false===$attributes=$this->validate($attributes, true))
 752 |             return false; 
 753 | 
 754 |         // Model Condition
 755 |         $query = $this->_findByCondition($condition);
 756 | 
 757 |         $attributes = $this->_attrEventBeforeUpdate($attributes);
 758 | 
 759 |         if ($this->fillable) $attributes = array_intersect_key($attributes, array_flip($this->fillable));
 760 |         
 761 |         // Pack query then move it to write DB from read DB
 762 |         $sql = $this->_dbr->set($attributes)->get_compiled_update();
 763 |         $this->_dbr->reset_query();
 764 | 
 765 |         return $this->_db->query($sql);
 766 |     }
 767 | 
 768 |     /**
 769 |      * Update a batch of update queries into combined query strings.
 770 |      *
 771 |      * @param array $dataSet [[[Attributes], [Condition]], ]
 772 |      * @param boolean $withAll withAll() switch helper
 773 |      * @param integer $maxLenth MySQL max_allowed_packet
 774 |      * @param boolean $runValidation Whether to perform validation (calling validate()) before manipulate the record. 
 775 |      * @return integer Count of successful query pack(s)
 776 |      * @example 
 777 |      *  $result = $this->Model->batchUpdate([
 778 |      *      [['title'=>'A1', 'modified'=>'1'], ['id'=>1]],
 779 |      *      [['title'=>'A2', 'modified'=>'1'], ['id'=>2]],
 780 |      *  ];);
 781 |      */
 782 |     public function batchUpdate(Array $dataSet, $withAll=false, $maxLength=null, $runValidation=true)
 783 |     {
 784 |         $maxLength = $maxLength ?: 4 * 1024 * 1024;
 785 | 
 786 |         $count = 0;
 787 |         $sqlBatch = '';
 788 |         
 789 |         foreach ($dataSet as $key => &$each) {
 790 | 
 791 |             // Data format
 792 |             list($attributes, $condition) = $each;
 793 | 
 794 |             // Check attributes
 795 |             if (!is_array($attributes) || !$attributes)
 796 |                 continue; 
 797 | 
 798 |             // Validation
 799 |             if ($runValidation && false===$attributes=$this->validate($attributes, true))
 800 |                 continue; 
 801 | 
 802 |             // WithAll helper
 803 |             if ($withAll===true) {
 804 |                 $this->withAll();
 805 |             }
 806 |                 
 807 |             // Model Condition
 808 |             $query = $this->_findByCondition($condition);
 809 | 
 810 |             $attributes = $this->_attrEventBeforeUpdate($attributes);
 811 | 
 812 |             // Pack query then move it to write DB from read DB
 813 |             $sql = $this->_dbr->set($attributes)->get_compiled_update();
 814 |             $this->_dbr->reset_query();
 815 | 
 816 |             // Last batch check: First single query & Max length
 817 |             // The first single query needs to be sent ahead to prevent the limitation that PDO transaction could not 
 818 |             // use multiple SQL line in one query, but allows if the multi-line query is behind a single query. 
 819 |             if (($count==0 && $sqlBatch) || strlen($sqlBatch)>=$maxLength) {
 820 |                 // Each batch of query
 821 |                 $result = $this->_db->query($sqlBatch);
 822 |                 $sqlBatch = "";
 823 |                 $count = ($result) ? $count + 1 : $count;
 824 |             }
 825 |             
 826 |             // Keep Combining query
 827 |             $sqlBatch .= "{$sql};\n";
 828 |         }
 829 | 
 830 |         // Last batch of query
 831 |         $result = $this->_db->query($sqlBatch);
 832 | 
 833 |         return ($result) ? $count + 1 : $count;
 834 |     }
 835 | 
 836 |     /**
 837 |      * Delete the selected record(s) with Timestamps feature into the associated database table.
 838 |      * 
 839 |      * @param mixed $condition Refer to _findByCondition() for the explanation 
 840 |      * @param boolean $forceDelete Force to hard delete
 841 |      * @param array $attributes Extended attributes for Soft Delete Mode
 842 |      * @return bool Result
 843 |      *
 844 |      * @example    
 845 |      *  $this->Model->delete(123);
 846 |      * @example
 847 |      *  // Query builder ORM usage
 848 |      *  $this->Model->find()->where('id', 123);
 849 |      *  $this->Model->delete();
 850 |      * @example  
 851 |      *  // Force delete for SOFT_DELETED mode 
 852 |      *  $this->Model->delete(123, true);
 853 |      */
 854 |     public function delete($condition=NULL, $forceDelete=false, $attributes=[])
 855 |     {
 856 |         // Check is Active Record
 857 |         if ($this->_readProperties) {
 858 |             // Reset condition and find single by self condition
 859 |             $this->reset();
 860 |             $condition = $this->_selfCondition;
 861 |         }
 862 |         
 863 |         // Model Condition by $forceDelete switch
 864 |         $query = ($forceDelete)
 865 |             ? $this->withTrashed()->_findByCondition($condition)
 866 |             : $this->_findByCondition($condition);
 867 | 
 868 |         /* Soft Delete Mode */
 869 |         if (static::SOFT_DELETED 
 870 |             && isset($this->softDeletedTrueValue) 
 871 |             && !$forceDelete) {
 872 |             
 873 |             // Mark the records as deleted
 874 |             $attributes[static::SOFT_DELETED] = $this->softDeletedTrueValue;
 875 | 
 876 |             $attributes = $this->_attrEventBeforeDelete($attributes);
 877 | 
 878 |             // Pack query then move it to write DB from read DB
 879 |             $sql = $this->_dbr->set($attributes)->get_compiled_update();
 880 |             $this->_dbr->reset_query();
 881 | 
 882 |         } else {
 883 | 
 884 |             /* Hard Delete */
 885 |             // Pack query then move it to write DB from read DB
 886 |             $sql = $this->_dbr->get_compiled_delete();
 887 |             $this->_dbr->reset_query();
 888 |         }
 889 |         
 890 |         return $this->_db->query($sql);
 891 |     }
 892 | 
 893 |     /**
 894 |      * Force Delete the selected record(s) with Timestamps feature into the associated database table.
 895 |      * 
 896 |      * @param mixed $condition Refer to _findByCondition() for the explanation 
 897 |      * @return mixed CI delete result of DB Query Builder
 898 |      *
 899 |      * @example    
 900 |      *  $this->Model->forceDelete(123)
 901 |      * @example
 902 |      *  // Query builder ORM usage
 903 |      *  $this->Model->find()->where('id', 123);
 904 |      *  $this->Model->forceDelete();
 905 |      */
 906 |     public function forceDelete($condition=NULL)
 907 |     {
 908 |         return $this->delete($condition, true);
 909 |     }
 910 | 
 911 |     /**
 912 |      * Get the number of affected rows when doing “write” type queries (insert, update, etc.).
 913 |      *
 914 |      * @return integer Last insert ID
 915 |      */
 916 |     public function getAffectedRows()
 917 |     {
 918 |         return $this->getDB()->affected_rows();
 919 |     }
 920 | 
 921 |     /**
 922 |      * Restore SOFT_DELETED field value to the selected record(s) into the associated database table.
 923 |      * 
 924 |      * @param mixed $condition Refer to _findByCondition() for the explanation 
 925 |      * @return bool Result
 926 |      *
 927 |      * @example    
 928 |      *  $this->Model->restore(123)
 929 |      * @example
 930 |      *  // Query builder ORM usage
 931 |      *  $this->Model->withTrashed()->find()->where('id', 123);
 932 |      *  $this->Model->restore();
 933 |      */
 934 |     public function restore($condition=NULL)
 935 |     {
 936 |         // Model Condition with Trashed
 937 |         $query = $this->withTrashed()->_findByCondition($condition);
 938 | 
 939 |         /* Soft Delete Mode */
 940 |         if (static::SOFT_DELETED 
 941 |             && isset($this->softDeletedFalseValue)) {
 942 |             
 943 |             // Mark the records as deleted
 944 |             $attributes[static::SOFT_DELETED] = $this->softDeletedFalseValue;
 945 | 
 946 |             return $query->update($this->table, $attributes);
 947 | 
 948 |         } else {
 949 | 
 950 |             return false;
 951 |         }
 952 |     }
 953 | 
 954 |     /**
 955 |      * Get count from query
 956 |      *
 957 |      * @param boolean Reset query conditions
 958 |      * @return integer
 959 |      */
 960 |     public function count($resetQuery=true)
 961 |     {
 962 |         return $this->getDBR()->count_all_results('', $resetQuery);
 963 |     }
 964 | 
 965 |     /**
 966 |      * Lock the selected rows in the table for updating.
 967 |      * 
 968 |      * sharedLock locks only for write, lockForUpdate also prevents them from being selected
 969 |      *
 970 |      * @example 
 971 |      *  $this->Model->find()->where('id', 123)
 972 |      *  $result = $this->Model->lockForUpdate()->row_array();
 973 |      * @example
 974 |      *  // This transaction block will lock selected rows for next same selected
 975 |      *  // rows with `FOR UPDATE` lock:
 976 |      *  $this->Model->getDB()->trans_start();
 977 |      *  $this->Model->find()->where('id', 123)
 978 |      *  $result = $this->Model->lockForUpdate()->row_array();
 979 |      *  $this->Model->getDB()->trans_complete();  
 980 |      * 
 981 |      * @return object CI_DB_result
 982 |      */
 983 |     public function lockForUpdate()
 984 |     {
 985 |         // Pack query then move it to write DB from read DB for transaction
 986 |         $sql = $this->_dbr->get_compiled_select();
 987 |         $this->_dbr->reset_query();
 988 | 
 989 |         return $this->_db->query("{$sql} FOR UPDATE");
 990 |     }
 991 | 
 992 |     /**
 993 |      * Share lock the selected rows in the table.
 994 |      * 
 995 |      * @example 
 996 |      *  $this->Model->find()->where('id', 123)
 997 |      *  $result = $this->Model->sharedLock()->row_array();'
 998 |      * 
 999 |      * @return object CI_DB_result
1000 |      */
1001 |     public function sharedLock()
1002 |     {
1003 |         // Pack query then move it to write DB from read DB for transaction
1004 |         $sql = $this->_dbr->get_compiled_select();
1005 |         $this->_dbr->reset_query();
1006 | 
1007 |         return $this->_db->query("{$sql} LOCK IN SHARE MODE");
1008 |     }
1009 | 
1010 |     /**
1011 |      * Without SOFT_DELETED query conditions for next find()
1012 |      *
1013 |      * @return $this
1014 |      * @example 
1015 |      *  $this->Model->withTrashed()->find();
1016 |      */
1017 |     public function withTrashed()
1018 |     {
1019 |         $this->_withoutSoftDeletedScope = true;
1020 | 
1021 |         return $this;
1022 |     }
1023 | 
1024 |     /**
1025 |      * Without Global Scopes query conditions for next find()
1026 |      *
1027 |      * @return $this
1028 |      * @example 
1029 |      *  $this->Model->withoutGlobalScopes()->find();
1030 |      */
1031 |     public function withoutGlobalScopes()
1032 |     {
1033 |         $this->_withoutGlobalScope = true;
1034 | 
1035 |         return $this;
1036 |     }
1037 | 
1038 |     /**
1039 |      * Without all query conditions for next find()
1040 |      * That is, with all set of Models for next find()
1041 |      *
1042 |      * @return $this
1043 |      * @example 
1044 |      *  $this->Model->withAll()->find();
1045 |      */
1046 |     public function withAll()
1047 |     {
1048 |         // Turn off switches of all featured conditions
1049 |         $this->withTrashed();
1050 |         $this->withoutGlobalScopes();
1051 | 
1052 |         return $this;
1053 |     }
1054 | 
1055 |     /**
1056 |      * New a Active Record from Model by data
1057 |      *
1058 |      * @param array $readProperties
1059 |      * @param array $selfCondition
1060 |      * @return object ActiveRecord(Model)
1061 |      */
1062 |     public function createActiveRecord($readProperties, $selfCondition)
1063 |     {
1064 |         $activeRecord = new static();
1065 |         // ORM handling
1066 |         $activeRecord->_readProperties = $readProperties;
1067 |         // Primary key condition to ensure single query result 
1068 |         $activeRecord->_selfCondition = $selfCondition;
1069 | 
1070 |         return $activeRecord;
1071 |     }
1072 | 
1073 |     /**
1074 |      * Active Record (ORM) save for insert or update
1075 |      *
1076 |      * @param boolean $runValidation Whether to perform validation (calling validate()) before manipulate the record. 
1077 |      * @return bool Result of CI insert
1078 |      */
1079 |     public function save($runValidation=true)
1080 |     {
1081 |         // if (empty($this->_writeProperties))
1082 |         //     return false;
1083 |         
1084 |         // ORM status distinguishing
1085 |         if (!$this->_selfCondition) {
1086 | 
1087 |             // Event
1088 |             if (!$this->beforeSave(true)) {
1089 |                 return false;
1090 |             }
1091 | 
1092 |             $result = $this->insert($this->_writeProperties, $runValidation);
1093 |             // Change this ActiveRecord to update mode
1094 |             if ($result) {
1095 |                 // ORM handling
1096 |                 $this->_readProperties = $this->_writeProperties;
1097 |                 $insertID =  $this->getLastInsertID();
1098 |                 $this->_readProperties[$this->primaryKey] = $insertID;
1099 |                 $this->_selfCondition = $insertID;
1100 |                 // Event
1101 |                 $this->afterSave(true, $this->_readProperties);
1102 |                 // Reset properties
1103 |                 $this->_writeProperties = [];
1104 |             }
1105 | 
1106 |         } else {
1107 | 
1108 |             // Event
1109 |             if (!$this->beforeSave(false)) {
1110 |                 return false;
1111 |             }
1112 |             
1113 |             $result = ($this->_writeProperties) ? $this->update($this->_writeProperties, $this->_selfCondition, $runValidation) : true;
1114 |             // Check the primary key is changed
1115 |             if ($result) {
1116 | 
1117 |                 // Primary key condition to ensure single query result 
1118 |                 if (isset($this->_writeProperties[$this->primaryKey])) {
1119 |                     $this->_selfCondition = $this->_writeProperties[$this->primaryKey];
1120 |                 }
1121 |                 $this->_readProperties = array_merge($this->_readProperties, $this->_writeProperties);
1122 |                 // Event
1123 |                 $this->afterSave(true, $this->_readProperties);
1124 |                 // Reset properties
1125 |                 $this->_writeProperties = [];
1126 |             }
1127 |         }
1128 |         
1129 |         return $result;
1130 |     }
1131 | 
1132 |     /**
1133 |      * This method is called at the beginning of inserting or updating a active record
1134 |      *
1135 |      * @param bool $insert whether this method called while inserting a record.
1136 |      * If `false`, it means the method is called while updating a record.
1137 |      * @return bool whether the insertion or updating should continue.
1138 |      * If `false`, the insertion or updating will be cancelled.
1139 |      */
1140 |     public function beforeSave($insert)
1141 |     {
1142 |         // overriding
1143 |         return true;
1144 |     }
1145 | 
1146 |     /**
1147 |      * This method is called at the end of inserting or updating a active record
1148 |      *
1149 |      * @param bool $insert whether this method called while inserting a record.
1150 |      * If `false`, it means the method is called while updating a record.
1151 |      * @param array $changedAttributes The old values of attributes that had changed and were saved.
1152 |      * You can use this parameter to take action based on the changes made for example send an email
1153 |      * when the password had changed or implement audit trail that tracks all the changes.
1154 |      * `$changedAttributes` gives you the old attribute values while the active record (`$this`) has
1155 |      * already the new, updated values.
1156 |      */
1157 |     public function afterSave($insert, $changedAttributes)
1158 |     {
1159 |         // overriding
1160 |     }
1161 | 
1162 |     /**
1163 |      * Declares a has-many relation.
1164 |      *
1165 |      * @param string $modelName The model class name of the related record
1166 |      * @param string $foreignKey 
1167 |      * @param string $localKey
1168 |      * @return \CI_DB_query_builder CI_DB_query_builder
1169 |      */
1170 |     public function hasMany($modelName, $foreignKey=null, $localKey=null)
1171 |     {
1172 |         return $this->_relationship($modelName, __FUNCTION__, $foreignKey, $localKey);
1173 |     }
1174 | 
1175 |     /**
1176 |      * Declares a has-many relation.
1177 |      *
1178 |      * @param string $modelName The model class name of the related record
1179 |      * @param string $foreignKey 
1180 |      * @param string $localKey
1181 |      * @return \CI_DB_query_builder CI_DB_query_builder
1182 |      */
1183 |     public function hasOne($modelName, $foreignKey=null, $localKey=null)
1184 |     {
1185 |         return $this->_relationship($modelName, __FUNCTION__, $foreignKey, $localKey);
1186 |     }
1187 | 
1188 |     /**
1189 |      * Base relationship.
1190 |      *
1191 |      * @param string $modelName The model class name of the related record
1192 |      * @param string $relationship
1193 |      * @param string $foreignKey 
1194 |      * @param string $localKey
1195 |      * @return \CI_DB_query_builder CI_DB_query_builder
1196 |      */
1197 |     protected function _relationship($modelName, $relationship, $foreignKey=null, $localKey=null)
1198 |     {
1199 |         /**
1200 |          * PSR-4 support check
1201 |          * 
1202 |          * @see https://github.com/yidas/codeigniter-psr4-autoload
1203 |          */
1204 |         if (strpos($modelName, "\\") !== false ) {
1205 |             
1206 |             $model = new $modelName;
1207 | 
1208 |         } else {
1209 |             // Original CodeIgniter 3 model loader
1210 |             get_instance()->load->model($modelName);
1211 |             // Fix the modelName if it has path
1212 |             $path = explode('/', $modelName);
1213 |             $modelName = count($path) > 1 ? end($path) : $modelName;
1214 |             $model = get_instance()->$modelName;
1215 |         }
1216 | 
1217 |         $libClass = __CLASS__;
1218 |         
1219 |         // Check if is using same library
1220 |         if (!is_subclass_of($model, $libClass)) {
1221 |             throw new Exception("Model `{$modelName}` does not extend {$libClass}", 500);
1222 |         }
1223 | 
1224 |         // Keys
1225 |         $foreignKey = ($foreignKey) ? $foreignKey : $this->primaryKey;
1226 |         $localKey = ($localKey) ? $localKey : $this->primaryKey; 
1227 | 
1228 |         $query = $model->find()
1229 |             ->where($foreignKey, $this->$localKey);
1230 | 
1231 |         // Inject Model name into query builder for ORM relationships
1232 |         $query->modelName = $modelName;
1233 |         // Inject relationship type into query builder for ORM relationships
1234 |         $query->relationship = $relationship;
1235 | 
1236 |         return $query;
1237 |     }
1238 | 
1239 |     /**
1240 |      * Get relationship property value
1241 |      *
1242 |      * @param string $method
1243 |      * @return mixed
1244 |      */
1245 |     protected function _getRelationshipProperty($method)
1246 |     {
1247 |         // Cache check
1248 |         if (isset($this->_relationshipCaches[$method])) {
1249 |             return $this->_relationshipCaches[$method];
1250 |         }
1251 |         
1252 |         $query = call_user_func_array([$this, $method], []);
1253 | 
1254 |         // Extract query builder injection property
1255 |         $modelName = isset($query->modelName) ? $query->modelName : null;
1256 |         $relationship = isset($query->relationship) ? $query->relationship : null;
1257 | 
1258 |         if (!$modelName || !$relationship) {
1259 |             throw new Exception("ORM relationships error", 500);
1260 |         }
1261 | 
1262 |         /**
1263 |          * PSR-4 support check
1264 |          * 
1265 |          * @see https://github.com/yidas/codeigniter-psr4-autoload
1266 |          */
1267 |         if (strpos($modelName, "\\") !== false ) {
1268 |             
1269 |             $model = new $modelName;
1270 | 
1271 |         } else {
1272 |             // Original CodeIgniter 3 model loader
1273 |             get_instance()->load->model($modelName);
1274 |             $model = get_instance()->$modelName;
1275 |         }
1276 | 
1277 |         // Check return type
1278 |         $result = ($relationship == 'hasOne') ? $model->findOne(null) : $model->findAll(null);
1279 | 
1280 |         // Save cache
1281 |         $this->_relationshipCaches[$method] = $result;
1282 | 
1283 |         return $result;
1284 |     }
1285 | 
1286 |     /**
1287 |      * Active Record transform to array record
1288 |      *
1289 |      * @return array
1290 |      * @example $record = $activeRecord->toArray();
1291 |      */
1292 |     public function toArray()
1293 |     {
1294 |         return $this->_readProperties;
1295 |     }
1296 |     
1297 |     /**
1298 |      * Get table schema
1299 |      *
1300 |      * @return array Column names
1301 |      */
1302 |     public function getTableSchema()
1303 |     {
1304 |         $class = get_class($this);
1305 | 
1306 |         // Check ORM Schema cache
1307 |         if (!isset(self::$_ormCaches[$class])) {
1308 | 
1309 |             $columns = $this->_dbr->query("SHOW COLUMNS FROM `{$this->table}`;")
1310 |                 ->result_array();
1311 | 
1312 |             // Cache
1313 |             self::$_ormCaches[$class] = $columns;
1314 |         }
1315 | 
1316 |         return self::$_ormCaches[$class];
1317 |     }
1318 | 
1319 |     /**
1320 |      * Index by Key
1321 |      *
1322 |      * @param array  $array Array data for handling
1323 |      * @param string $key  Array key for index key
1324 |      * @param bool   $obj2Array Object converts to array if is object
1325 |      * @return array Result with indexBy Key
1326 |      * @example 
1327 |      *  $records = $this->Model->findAll();
1328 |      *  $this->Model->indexBy($records, 'sn');
1329 |      */
1330 |     public static function indexBy(Array &$array, $key=null, $obj2Array=false)
1331 |     {
1332 |         // Use model instance's primary key while no given key
1333 |         $key = ($key) ?: (new static())->primaryKey;
1334 | 
1335 |         $tmp = [];
1336 |         foreach ($array as $row) {
1337 |             // Array & Object types support 
1338 |             if (is_object($row) && isset($row->$key)) {
1339 |                 
1340 |                 $tmp[$row->$key] = ($obj2Array) ? (array)$row : $row;
1341 |             } 
1342 |             elseif (is_array($row) && isset($row[$key])) {
1343 |                 
1344 |                 $tmp[$row[$key]] = $row;
1345 |             }
1346 |         }
1347 |         return $array = $tmp;
1348 |     }
1349 | 
1350 |     /**
1351 |      * Encodes special characters into HTML entities.
1352 |      * 
1353 |      * The [[$this->config->item('charset')]] will be used for encoding.
1354 |      * 
1355 |      * @param string $content the content to be encoded
1356 |      * @param bool $doubleEncode whether to encode HTML entities in `$content`. If false,
1357 |      * HTML entities in `$content` will not be further encoded.
1358 |      * @return string the encoded content
1359 |      * 
1360 |      * @see http://www.php.net/manual/en/function.htmlspecialchars.php
1361 |      * @see https://www.yiiframework.com/doc/api/2.0/yii-helpers-basehtml#encode()-detail
1362 |      */
1363 |     public static function htmlEncode($content, $doubleEncode = true)
1364 |     {
1365 |         $ci = & get_instance();
1366 |         
1367 |         return htmlspecialchars($content, ENT_QUOTES | ENT_SUBSTITUTE, $ci->config->item('charset') ? $ci->config->item('charset') : 'UTF-8', $doubleEncode);
1368 |     }
1369 | 
1370 |     /**
1371 |      * Decodes special HTML entities back to the corresponding characters.
1372 |      * 
1373 |      * This is the opposite of [[encode()]].
1374 |      * 
1375 |      * @param string $content the content to be decoded
1376 |      * @return string the decoded content
1377 |      * @see htmlEncode()
1378 |      * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php
1379 |      * @see https://www.yiiframework.com/doc/api/2.0/yii-helpers-basehtml#decode()-detail
1380 |      */
1381 |     public static function htmlDecode($content)
1382 |     {
1383 |         return htmlspecialchars_decode($content, ENT_QUOTES);
1384 |     }
1385 | 
1386 |     /**
1387 |      * Query Scopes Handler
1388 |      *
1389 |      * @return bool Result
1390 |      */
1391 |     protected function _globalScopes()
1392 |     {
1393 |         // Events for inheriting
1394 | 
1395 |         return true;
1396 |     }
1397 | 
1398 |     /**
1399 |      * Attributes handle function for each Insert
1400 |      *
1401 |      * @param array $attributes
1402 |      * @return array Addon $attributes of pointer
1403 |      */
1404 |     protected function _attrEventBeforeInsert(&$attributes)
1405 |     {
1406 |         $this->_formatDate(static::CREATED_AT, $attributes);
1407 | 
1408 |         // Trigger UPDATED_AT
1409 |         if ($this->createdWithUpdated) {
1410 |             
1411 |             $this->_formatDate(static::UPDATED_AT, $attributes);
1412 |         }
1413 | 
1414 |         return $attributes;
1415 |     }
1416 | 
1417 |     /**
1418 |      * Attributes handle function for Update
1419 |      *
1420 |      * @param array $attributes
1421 |      * @return array Addon $attributes of pointer
1422 |      */
1423 |     protected function _attrEventBeforeUpdate(&$attributes)
1424 |     {
1425 |         $this->_formatDate(static::UPDATED_AT, $attributes);
1426 | 
1427 |         return $attributes;
1428 |     }
1429 | 
1430 |     /**
1431 |      * Attributes handle function for Delete
1432 |      *
1433 |      * @param array $attributes
1434 |      * @return array Addon $attributes of pointer
1435 |      */
1436 |     protected function _attrEventBeforeDelete(&$attributes)
1437 |     {
1438 |         $this->_formatDate(static::DELETED_AT, $attributes);
1439 | 
1440 |         return $attributes;
1441 |     }
1442 | 
1443 |     /**
1444 |      * Finds record(s) by the given condition with a fresh query.
1445 |      *
1446 |      * This method is internally called by findOne(), findAll(), update(), delete(), etc.
1447 |      * The query will be reset to start a new scope if the condition is used.
1448 |      * 
1449 |      * @param mixed Primary key value or a set of column values. If is null, it would be used for  
1450 |      *  previous find() method, which means it would not rebuild find() so it would check and 
1451 |      *  protect the SQL statement.
1452 |      * @return \CI_DB_query_builder CI_DB_query_builder
1453 |      * @internal
1454 |      * @example 
1455 |      *  // find a single customer whose primary key value is 10
1456 |      *  $this->_findByCondition(10);
1457 |      *
1458 |      *  // find the customers whose primary key value is 10, 11 or 12.
1459 |      *  $this->_findByCondition([10, 11, 12]);
1460 |      *
1461 |      *  // find the first customer whose age is 30 and whose status is 1
1462 |      *  $this->_findByCondition(['age' => 30, 'status' => 1]);
1463 |      */
1464 |     protected function _findByCondition($condition=null)
1465 |     {
1466 |         // Reset Query if condition existed
1467 |         if ($condition !== null) {
1468 |             $this->_dbr->reset_query();
1469 |             $query = $this->find();
1470 |         } else {
1471 |             // Support for previous find(), no need to find() again
1472 |             $query = $this->_dbr;
1473 |         }
1474 | 
1475 |         // Check condition type
1476 |         if (is_array($condition)) {
1477 | 
1478 |             // Check if is numeric array
1479 |             if (array_keys($condition)===range(0, count($condition)-1)) {
1480 |                 
1481 |                 /* Numeric Array */
1482 |                 $query->where_in($this->_field($this->primaryKey), $condition);
1483 | 
1484 |             } else {
1485 | 
1486 |                 /* Associated Array */
1487 |                 foreach ($condition as $field => $value) {
1488 |                     
1489 |                     (is_array($value)) ? $query->where_in($field, $value) : $query->where($field, $value);
1490 |                 }
1491 |             }
1492 |         } 
1493 |         elseif (is_numeric($condition) || is_string($condition)) {
1494 |             /* Single Primary Key */
1495 |             $query->where($this->_field($this->primaryKey), $condition);
1496 |         }
1497 |         else {
1498 |             // Simply Check SQL for no condition such as update/delete
1499 |             // Warning: This protection just simply check keywords that may not find out for some situations.
1500 |             $sql = $this->_dbr->get_compiled_select('', false); // No reset query
1501 |             // Check FROM for table condition
1502 |             if (stripos($sql, 'from ')===false)
1503 |                 throw new Exception("You should find() first, or use condition array for update/delete", 400);
1504 |             // No condition situation needs to enable where protection
1505 |             if (stripos($sql, 'where ')===false)
1506 |                 throw new Exception("You could not update/delete without any condition! Use find()->where('1=1') or condition array at least.", 400);
1507 |         }
1508 | 
1509 |         return $query;
1510 |     }
1511 | 
1512 |     /**
1513 |      * Format a date for timestamps
1514 |      *
1515 |      * @param string Field name
1516 |      * @param array Attributes
1517 |      * @return array Addon $attributes of pointer
1518 |      */
1519 |     protected function _formatDate($field, &$attributes)
1520 |     {
1521 |         if ($this->timestamps && $field) {
1522 | 
1523 |             switch ($this->dateFormat) {
1524 |                 case 'datetime':
1525 |                     $dateFormat = date("Y-m-d H:i:s");
1526 |                     break;
1527 |                 
1528 |                 case 'unixtime':
1529 |                 default:
1530 |                     $dateFormat = time();
1531 |                     break;
1532 |             }
1533 |             
1534 |             $attributes[$field] = $dateFormat;
1535 |         }
1536 | 
1537 |         return $attributes;
1538 |     }
1539 | 
1540 |     /**
1541 |      * The scope which not been soft deleted 
1542 |      *
1543 |      * @param bool $skip Skip
1544 |      * @return bool Result
1545 |      */
1546 |     protected function _addSoftDeletedCondition()
1547 |     {
1548 |         if ($this->_withoutSoftDeletedScope) {
1549 |             // Reset SOFT_DELETED switch
1550 |             $this->_withoutSoftDeletedScope = false;
1551 |         } 
1552 |         elseif (static::SOFT_DELETED && isset($this->softDeletedFalseValue)) {
1553 |             // Add condition
1554 |             $this->_dbr->where($this->_field(static::SOFT_DELETED), 
1555 |             $this->softDeletedFalseValue);
1556 |         }
1557 |         
1558 |         return true;
1559 |     }
1560 | 
1561 |     /**
1562 |      * The scope which not been soft deleted 
1563 |      *
1564 |      * @param bool $skip Skip
1565 |      * @return bool Result
1566 |      */
1567 |     protected function _addGlobalScopeCondition()
1568 |     {
1569 |         if ($this->_withoutGlobalScope) {
1570 |             // Reset Global Switch switch
1571 |             $this->_withoutGlobalScope = false;
1572 | 
1573 |         } else {
1574 |             // Default to apply global scopes
1575 |             $this->_globalScopes();
1576 |         }
1577 | 
1578 |         return true;
1579 |     }
1580 | 
1581 |     /**
1582 |      * Standardize field name
1583 |      *
1584 |      * @param string $columnName
1585 |      * @return string Standardized column name
1586 |      */
1587 |     protected function _field($columnName)
1588 |     {
1589 |         if ($this->alias) {
1590 |             return "`{$this->alias}`.`{$columnName}`";
1591 |         }
1592 |         
1593 |         if ($this->_db->dbprefix) {
1594 |             return "{$this->table}.`{$columnName}`";
1595 |         }
1596 |         
1597 |         return "`{$this->table}`.`{$columnName}`";
1598 |     }
1599 | 
1600 |     /**
1601 |      * Get & load $this->db in CI application
1602 |      * 
1603 |      * @return object CI $this->db
1604 |      */
1605 |     private function _getDefaultDB()
1606 |     {
1607 |         // For ReadDatabase checking Master first
1608 |         if ($this->_db) {
1609 |             return $this->_db;
1610 |         }
1611 |         
1612 |         if (!isset($this->db)) {
1613 |             get_instance()->load->database();
1614 |         }
1615 |         // No need to set as reference because $this->db is refered to &DB already.
1616 |         return get_instance()->db;
1617 |     }
1618 | 
1619 |     /**
1620 |      * ORM set property
1621 |      *
1622 |      * @param string $name Property key name
1623 |      * @param mixed $value
1624 |      */
1625 |     public function __set($name, $value)
1626 |     {
1627 |         // Property check option
1628 |         if ($this->propertyCheck) {
1629 | 
1630 |             $flag = false;
1631 |             
1632 |             // Check if exists
1633 |             foreach ($this->getTableSchema() as $key => $column) {
1634 |                 if ($name == $column['Field']) {
1635 |                     $flag = true;
1636 |                 }
1637 |             }
1638 | 
1639 |             // No mathc Exception
1640 |             if (!$flag) {
1641 |                 throw new \Exception("Property `{$name}` does not exist", 500);  
1642 |             }
1643 |         }
1644 |         
1645 |         $this->_writeProperties[$name] = $value;
1646 |     }
1647 | 
1648 |     /**
1649 |      * ORM get property
1650 |      *
1651 |      * @param string $name Property key name
1652 |      */
1653 |     public function __get($name)
1654 |     {
1655 |         // ORM property check
1656 |         if (array_key_exists($name, $this->_writeProperties) ) {
1657 |             
1658 |             return $this->_writeProperties[$name]; 
1659 |         }
1660 |         else if (array_key_exists($name, $this->_readProperties)) {
1661 |             
1662 |             return $this->_readProperties[$name]; 
1663 |         }
1664 |         // ORM relationship check
1665 |         else if (method_exists($this, $method = $name)) {
1666 |             
1667 |             return $this->_getRelationshipProperty($method);
1668 |         }
1669 |         // ORM schema check
1670 |         else {
1671 | 
1672 |             // Write cache to read properties of this ORM
1673 |             foreach ($this->getTableSchema() as $key => $column) {
1674 | 
1675 |                 $this->_readProperties[$column['Field']] = isset($this->_readProperties[$column['Field']]) 
1676 |                     ? $this->_readProperties[$column['Field']] 
1677 |                     : null;
1678 |             }
1679 | 
1680 |             // Match property again
1681 |             if (array_key_exists($name, $this->_readProperties)) {
1682 |         
1683 |                 return $this->_readProperties[$name]; 
1684 |             }
1685 | 
1686 |             // CI parent::__get() check
1687 |             if (property_exists(get_instance(), $name)) {
1688 |                 
1689 |                 return parent::__get($name);
1690 |             }
1691 | 
1692 |             // Exception
1693 |             throw new \Exception("Property `{$name}` does not exist", 500);  
1694 |         }
1695 | 
1696 |         return null;
1697 |     }
1698 |     
1699 |     /**
1700 |      * ORM isset property
1701 |      *
1702 |      * @param string $name
1703 |      * @return void
1704 |      */
1705 |     public function __isset($name) {
1706 | 
1707 |         if (isset($this->_writeProperties[$name])) {
1708 |             
1709 |             return true;
1710 |         }
1711 |         else if (isset($this->_readProperties[$name])) {
1712 | 
1713 |             return true;
1714 |         }
1715 |         else if (method_exists($this, $method = $name)) {
1716 |             
1717 |             return ($this->_getRelationshipProperty($method));
1718 |         }
1719 | 
1720 |         return false;
1721 |     }
1722 | 
1723 |     /**
1724 |      * ORM unset property
1725 |      *
1726 |      * @param string $name
1727 |      * @return void
1728 |      */
1729 |     public function __unset($name) {
1730 | 
1731 |         unset($this->_writeProperties[$name]);
1732 |         unset($this->_readProperties[$name]);
1733 |     }
1734 |     
1735 |     /**
1736 |      * ArrayAccess offsetSet
1737 |      *
1738 |      * @param string $offset
1739 |      * @param mixed $value
1740 |      * @return void
1741 |      */
1742 |     public function offsetSet($offset, $value) {
1743 |         
1744 |         return $this->__set($offset, $value);
1745 |     }
1746 | 
1747 |     /**
1748 |      * ArrayAccess offsetExists
1749 |      *
1750 |      * @param string $offset
1751 |      * @return bool Result
1752 |      */
1753 |     public function offsetExists($offset) {
1754 | 
1755 |         return $this->__isset($offset);
1756 |     }
1757 | 
1758 |     /**
1759 |      * ArrayAccess offsetUnset
1760 |      *
1761 |      * @param string $offset
1762 |      * @return void
1763 |      */
1764 |     public function offsetUnset($offset) {
1765 | 
1766 |         return $this->__unset($offset);
1767 |     }
1768 | 
1769 |     /**
1770 |      * ArrayAccess offsetGet
1771 |      *
1772 |      * @param string $offset
1773 |      * @return mixed Value of property
1774 |      */
1775 |     public function offsetGet($offset) {
1776 | 
1777 |         return $this->$offset;
1778 |     }
1779 | }
1780 | 
--------------------------------------------------------------------------------