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