├── 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 |

CodeIgniter Model

6 |
7 |

8 | 9 | CodeIgniter 3 Active Record (ORM) Standard Model supported Read & Write Connections 10 | 11 | [![Latest Stable Version](https://poser.pugx.org/yidas/codeigniter-model/v/stable?format=flat-square)](https://packagist.org/packages/yidas/codeigniter-model) 12 | [![License](https://poser.pugx.org/yidas/codeigniter-model/license?format=flat-square)](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 | --------------------------------------------------------------------------------