├── .gitignore ├── tests ├── support │ ├── models │ │ ├── record_model.php │ │ ├── soft_delete_model.php │ │ ├── protected_attributes_model.php │ │ ├── validated_model.php │ │ ├── serialised_data_model.php │ │ ├── relationship_model.php │ │ ├── callback_parameter_model.php │ │ ├── after_callback_model.php │ │ └── before_callback_model.php │ ├── database.php │ └── test_helper.php └── MY_Model_test.php ├── composer.json ├── .travis.yml ├── phpunit.xml ├── LICENSE ├── README.md └── core └── MY_Model.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | vendor 3 | composer.lock 4 | composer.phar -------------------------------------------------------------------------------- /tests/support/models/record_model.php: -------------------------------------------------------------------------------- 1 | =0.7.2" 5 | }, 6 | "autoload": { 7 | "classmap": [ "core", "tests/support/models" ] 8 | } 9 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.3 4 | - 5.4 5 | 6 | before_script: 7 | - curl -s https://getcomposer.org/installer | php 8 | - php composer.phar install --dev 9 | 10 | script: vendor/bin/phpunit tests/MY_Model_test.php -------------------------------------------------------------------------------- /tests/support/models/soft_delete_model.php: -------------------------------------------------------------------------------- 1 | soft_delete_key = $key; 12 | } 13 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | ./tests/MY_Model_test.php 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/support/models/protected_attributes_model.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | /** 11 | * protected_attributes_model.php contains a test model with protected attributes 12 | */ 13 | 14 | class Protected_attributes_model extends MY_Model 15 | { 16 | public $protected_attributes = array( 'id', 'hash' ); 17 | } -------------------------------------------------------------------------------- /tests/support/models/validated_model.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | class Validated_model extends MY_Model 11 | { 12 | public $validate = array( 13 | array( 'field' => 'name', 'label' => 'Name', 'rules' => 'required' ), 14 | array( 'field' => 'sexyness', 'label' => 'Sexyness', 'rules' => 'required' ) 15 | ); 16 | } -------------------------------------------------------------------------------- /tests/support/models/serialised_data_model.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | /** 11 | * serialised_data_model.php contains a test model that includes serialising a columns 12 | */ 13 | 14 | class Serialised_data_model extends MY_Model 15 | { 16 | public $before_create = array( 'serialize(data)' ); 17 | public $before_update = array( 'serialize(data)' ); 18 | } -------------------------------------------------------------------------------- /tests/support/models/relationship_model.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | /** 11 | * relationship_model.php contains a test model that has a belongs_to and has_many relationship 12 | */ 13 | 14 | class Belongs_to_model extends MY_Model 15 | { 16 | public $belongs_to = array( 'author' ); 17 | public $has_many = array( 'comments' ); 18 | } 19 | 20 | class Author_model extends MY_Model 21 | { 22 | protected $_table = 'authors'; 23 | } -------------------------------------------------------------------------------- /tests/support/models/callback_parameter_model.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | /** 11 | * callback_parameter_model.php contains a test model that defines a callback 12 | * with embedded parameters 13 | */ 14 | 15 | class Callback_parameter_model extends MY_Model 16 | { 17 | public $callback = array('some_callback(some_param,another_param)'); 18 | 19 | public function some_method() 20 | { 21 | $this->trigger('callback'); 22 | } 23 | 24 | protected function some_callback() 25 | { 26 | throw new Callback_Test_Exception($this->callback_parameters); 27 | } 28 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Jamie Rumbelow 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /tests/support/database.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | /** 11 | * database.php is a fakeified CodeIgniter query builder 12 | */ 13 | 14 | class MY_Model_Mock_DB 15 | { 16 | /** 17 | * CI_DB 18 | */ 19 | public function select() { } 20 | public function where() { } 21 | public function where_in() { } 22 | public function get() { } 23 | public function from() { } 24 | public function insert() { } 25 | public function insert_id() { } 26 | public function set() { } 27 | public function update() { } 28 | public function delete() { } 29 | public function order_by() { } 30 | public function limit() { } 31 | public function count_all_results() { } 32 | public function count_all() { } 33 | public function truncate() { } 34 | 35 | /** 36 | * CI_DB_Result 37 | */ 38 | public function row() { } 39 | public function result() { } 40 | public function row_array() { } 41 | public function result_array() { } 42 | } -------------------------------------------------------------------------------- /tests/support/models/after_callback_model.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | /** 11 | * after_callback_model.php contains a test model that defines every after callback as a function 12 | * that throws an exception. We can then catch that in the tests to ensure callbacks work. 13 | */ 14 | 15 | class After_callback_model extends MY_Model 16 | { 17 | protected $after_create = array('test_throw'); 18 | protected $after_update = array('test_throw'); 19 | protected $after_get = array('test_data_callback', 'test_data_callback_two'); 20 | protected $after_delete = array('test_throw'); 21 | 22 | protected function test_throw($row) 23 | { 24 | throw new Callback_Test_Exception($row); 25 | } 26 | 27 | protected function test_data_callback($row) 28 | { 29 | $row['key'] = 'Value'; 30 | return $row; 31 | } 32 | 33 | protected function test_data_callback_two($row) 34 | { 35 | $row['another_key'] = '123 Value'; 36 | return $row; 37 | } 38 | } -------------------------------------------------------------------------------- /tests/support/models/before_callback_model.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | /** 11 | * before_callback_model.php contains a test model that defines every before callback as a function 12 | * that throws an exception. We can then catch that in the tests to ensure callbacks work. 13 | */ 14 | 15 | class Before_callback_model extends MY_Model 16 | { 17 | protected $before_create = array('test_data_callback', 'test_data_callback_two'); 18 | protected $before_update = array('test_data_callback', 'test_data_callback_two'); 19 | protected $before_get = array('test_throw'); 20 | protected $before_delete = array('test_throw'); 21 | 22 | protected function test_throw($row) 23 | { 24 | throw new Callback_Test_Exception($row); 25 | } 26 | 27 | protected function test_data_callback($row) 28 | { 29 | $row['key'] = 'Value'; 30 | return $row; 31 | } 32 | 33 | protected function test_data_callback_two($row) 34 | { 35 | $row['another_key'] = '123 Value'; 36 | return $row; 37 | } 38 | } -------------------------------------------------------------------------------- /tests/support/test_helper.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | /** 11 | * test_helper.php is the bootstrap file for our tests - it loads up an 12 | * appropriate faux-CodeIgniter environment for our tests to run in. 13 | */ 14 | 15 | // Load our MY_Model and the fakeish record model 16 | require_once 'vendor/autoload.php'; 17 | 18 | require_once 'tests/support/database.php'; 19 | 20 | 21 | /** 22 | * Fake the CodeIgniter base model! 23 | */ 24 | class CI_Model 25 | { 26 | public function __construct() 27 | { 28 | $this->load = new CI_Loader(); 29 | 30 | // Pretend CI has a loaded DB already. 31 | $this->db = new MY_Model_Mock_DB(); 32 | } 33 | } 34 | 35 | /** 36 | * The loads happen in the constructor (before we can mock anything out), 37 | * so instead we'll fakeify the Loader 38 | */ 39 | class CI_Loader 40 | { 41 | public function __call($method, $params = array()) {} 42 | } 43 | 44 | /** 45 | * ...but relationships load models, so fake that 46 | */ 47 | class MY_Model_Mock_Loader 48 | { 49 | public function model($name, $assigned_name = '') { } 50 | } 51 | 52 | /** 53 | * We also need to fake the inflector 54 | */ 55 | function singular($name) 56 | { 57 | return 'comment'; 58 | } 59 | 60 | function plural($name) 61 | { 62 | return 'records'; 63 | } 64 | 65 | /** 66 | * Let our tests know about our callbacks 67 | */ 68 | 69 | class MY_Model_Test_Exception extends Exception 70 | { 71 | public $passed_object = FALSE; 72 | 73 | public function __construct($passed_object, $message = '') 74 | { 75 | parent::__construct($message); 76 | $this->passed_object = $passed_object; 77 | } 78 | } 79 | 80 | class Callback_Test_Exception extends MY_Model_Test_Exception 81 | { 82 | public function __construct($passed_object) 83 | { 84 | parent::__construct($passed_object, 'Callback is being successfully thrown'); 85 | } 86 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DEPRECATED: codeigniter-base-model 2 | ===================================== 3 | 4 | **Deprecated since I no longer use CodeIgniter. If anybody would like to take over maintainence of this repo, please get in touch.** 5 | 6 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) [![Build Status](https://secure.travis-ci.org/jamierumbelow/codeigniter-base-model.png?branch=master)](http://travis-ci.org/jamierumbelow/codeigniter-base-model) 7 | 8 | My CodeIgniter Base Model is an extended CI_Model class to use in your CodeIgniter applications. It provides a full CRUD base to make developing database interactions easier and quicker, as well as an event-based observer system, in-model data validation, intelligent table name guessing and soft delete. 9 | 10 | Synopsis 11 | -------- 12 | 13 | ```php 14 | class Post_model extends MY_Model { } 15 | 16 | $this->load->model('post_model', 'post'); 17 | 18 | $this->post->get_all(); 19 | 20 | $this->post->get(1); 21 | $this->post->get_by('title', 'Pigs CAN Fly!'); 22 | $this->post->get_many_by('status', 'open'); 23 | 24 | $this->post->insert(array( 25 | 'status' => 'open', 26 | 'title' => "I'm too sexy for my shirt" 27 | )); 28 | 29 | $this->post->update(1, array( 'status' => 'closed' )); 30 | 31 | $this->post->delete(1); 32 | ``` 33 | 34 | Installation/Usage 35 | ------------------ 36 | 37 | Download and drag the MY\_Model.php file into your _application/core_ folder. CodeIgniter will load and initialise this class automatically for you. 38 | 39 | Extend your model classes from `MY_Model` and all the functionality will be baked in automatically. 40 | 41 | Naming Conventions 42 | ------------------ 43 | 44 | This class will try to guess the name of the table to use, by finding the plural of the class name. 45 | 46 | For instance: 47 | 48 | class Post_model extends MY_Model { } 49 | 50 | ...will guess a table name of `posts`. It also works with `_m`: 51 | 52 | class Book_m extends MY_Model { } 53 | 54 | ...will guess `books`. 55 | 56 | If you need to set it to something else, you can declare the _$\_table_ instance variable and set it to the table name: 57 | 58 | class Post_model extends MY_Model 59 | { 60 | public $_table = 'blogposts'; 61 | } 62 | 63 | Some of the CRUD functions also assume that your primary key ID column is called _'id'_. You can overwrite this functionality by setting the _$primary\_key_ instance variable: 64 | 65 | class Post_model extends MY_Model 66 | { 67 | public $primary_key = 'post_id'; 68 | } 69 | 70 | Callbacks/Observers 71 | ------------------- 72 | 73 | There are many times when you'll need to alter your model data before it's inserted or returned. This could be adding timestamps, pulling in relationships or deleting dependent rows. The MVC pattern states that these sorts of operations need to go in the model. In order to facilitate this, **MY_Model** contains a series of callbacks/observers -- methods that will be called at certain points. 74 | 75 | The full list of observers are as follows: 76 | 77 | * $before_create 78 | * $after_create 79 | * $before_update 80 | * $after_update 81 | * $before_get 82 | * $after_get 83 | * $before_delete 84 | * $after_delete 85 | 86 | These are instance variables usually defined at the class level. They are arrays of methods on this class to be called at certain points. An example: 87 | 88 | ```php 89 | class Book_model extends MY_Model 90 | { 91 | public $before_create = array( 'timestamps' ); 92 | 93 | protected function timestamps($book) 94 | { 95 | $book['created_at'] = $book['updated_at'] = date('Y-m-d H:i:s'); 96 | return $book; 97 | } 98 | } 99 | ``` 100 | 101 | **Remember to always always always return the `$row` object you're passed. Each observer overwrites its predecessor's data, sequentially, in the order the observers are defined.** 102 | 103 | Observers can also take parameters in their name, much like CodeIgniter's Form Validation library. Parameters are then accessed in `$this->callback_parameters`: 104 | 105 | public $before_create = array( 'data_process(name)' ); 106 | public $before_update = array( 'data_process(date)' ); 107 | 108 | protected function data_process($row) 109 | { 110 | $row[$this->callback_parameters[0]] = $this->_process($row[$this->callback_parameters[0]]); 111 | 112 | return $row; 113 | } 114 | 115 | Validation 116 | ---------- 117 | 118 | MY_Model uses CodeIgniter's built in form validation to validate data on insert. 119 | 120 | You can enable validation by setting the `$validate` instance to the usual form validation library rules array: 121 | 122 | class User_model extends MY_Model 123 | { 124 | public $validate = array( 125 | array( 'field' => 'email', 126 | 'label' => 'email', 127 | 'rules' => 'required|valid_email|is_unique[users.email]' ), 128 | array( 'field' => 'password', 129 | 'label' => 'password', 130 | 'rules' => 'required' ), 131 | array( 'field' => 'password_confirmation', 132 | 'label' => 'confirm password', 133 | 'rules' => 'required|matches[password]' ), 134 | ); 135 | } 136 | 137 | Anything valid in the form validation library can be used here. To find out more about the rules array, please [view the library's documentation](http://codeigniter.com/user_guide/libraries/form_validation.html#validationrulesasarray). 138 | 139 | With this array set, each call to `insert()` or `update()` will validate the data before allowing the query to be run. **Unlike the CodeIgniter validation library, this won't validate the POST data, rather, it validates the data passed directly through.** 140 | 141 | You can skip the validation with `skip_validation()`: 142 | 143 | $this->user_model->skip_validation(); 144 | $this->user_model->insert(array( 'email' => 'blah' )); 145 | 146 | Alternatively, pass through a `TRUE` to `insert()`: 147 | 148 | $this->user_model->insert(array( 'email' => 'blah' ), TRUE); 149 | 150 | Under the hood, this calls `validate()`. 151 | 152 | Protected Attributes 153 | -------------------- 154 | 155 | If you're lazy like me, you'll be grabbing the data from the form and throwing it straight into the model. While some of the pitfalls of this can be avoided with validation, it's a very dangerous way of entering data; any attribute on the model (any column in the table) could be modified, including the ID. 156 | 157 | To prevent this from happening, MY_Model supports protected attributes. These are columns of data that cannot be modified. 158 | 159 | You can set protected attributes with the `$protected_attributes` array: 160 | 161 | class Post_model extends MY_Model 162 | { 163 | public $protected_attributes = array( 'id', 'hash' ); 164 | } 165 | 166 | Now, when `insert` or `update` is called, the attributes will automatically be removed from the array, and, thus, protected: 167 | 168 | $this->post_model->insert(array( 169 | 'id' => 2, 170 | 'hash' => 'aqe3fwrga23fw243fWE', 171 | 'title' => 'A new post' 172 | )); 173 | 174 | // SQL: INSERT INTO posts (title) VALUES ('A new post') 175 | 176 | Relationships 177 | ------------- 178 | 179 | **MY\_Model** now has support for basic _belongs\_to_ and has\_many relationships. These relationships are easy to define: 180 | 181 | class Post_model extends MY_Model 182 | { 183 | public $belongs_to = array( 'author' ); 184 | public $has_many = array( 'comments' ); 185 | } 186 | 187 | It will assume that a MY_Model API-compatible model with the singular relationship's name has been defined. By default, this will be `relationship_model`. The above example, for instance, would require two other models: 188 | 189 | class Author_model extends MY_Model { } 190 | class Comment_model extends MY_Model { } 191 | 192 | If you'd like to customise this, you can pass through the model name as a parameter: 193 | 194 | class Post_model extends MY_Model 195 | { 196 | public $belongs_to = array( 'author' => array( 'model' => 'author_m' ) ); 197 | public $has_many = array( 'comments' => array( 'model' => 'model_comments' ) ); 198 | } 199 | 200 | You can then access your related data using the `with()` method: 201 | 202 | $post = $this->post_model->with('author') 203 | ->with('comments') 204 | ->get(1); 205 | 206 | The related data will be embedded in the returned value from `get`: 207 | 208 | echo $post->author->name; 209 | 210 | foreach ($post->comments as $comment) 211 | { 212 | echo $message; 213 | } 214 | 215 | Separate queries will be run to select the data, so where performance is important, a separate JOIN and SELECT call is recommended. 216 | 217 | The primary key can also be configured. For _belongs\_to_ calls, the related key is on the current object, not the foreign one. Pseudocode: 218 | 219 | SELECT * FROM authors WHERE id = $post->author_id 220 | 221 | ...and for a _has\_many_ call: 222 | 223 | SELECT * FROM comments WHERE post_id = $post->id 224 | 225 | To change this, use the `primary_key` value when configuring: 226 | 227 | class Post_model extends MY_Model 228 | { 229 | public $belongs_to = array( 'author' => array( 'primary_key' => 'post_author_id' ) ); 230 | public $has_many = array( 'comments' => array( 'primary_key' => 'parent_post_id' ) ); 231 | } 232 | 233 | Arrays vs Objects 234 | ----------------- 235 | 236 | By default, MY_Model is setup to return objects using CodeIgniter's QB's `row()` and `result()` methods. If you'd like to use their array counterparts, there are a couple of ways of customising the model. 237 | 238 | If you'd like all your calls to use the array methods, you can set the `$return_type` variable to `array`. 239 | 240 | class Book_model extends MY_Model 241 | { 242 | protected $return_type = 'array'; 243 | } 244 | 245 | If you'd like just your _next_ call to return a specific type, there are two scoping methods you can use: 246 | 247 | $this->book_model->as_array() 248 | ->get(1); 249 | $this->book_model->as_object() 250 | ->get_by('column', 'value'); 251 | 252 | Soft Delete 253 | ----------- 254 | 255 | By default, the delete mechanism works with an SQL `DELETE` statement. However, you might not want to destroy the data, you might instead want to perform a 'soft delete'. 256 | 257 | If you enable soft deleting, the deleted row will be marked as `deleted` rather than actually being removed from the database. 258 | 259 | Take, for example, a `Book_model`: 260 | 261 | class Book_model extends MY_Model { } 262 | 263 | We can enable soft delete by setting the `$this->soft_delete` key: 264 | 265 | class Book_model extends MY_Model 266 | { 267 | protected $soft_delete = TRUE; 268 | } 269 | 270 | By default, MY_Model expects a `TINYINT` or `INT` column named `deleted`. If you'd like to customise this, you can set `$soft_delete_key`: 271 | 272 | class Book_model extends MY_Model 273 | { 274 | protected $soft_delete = TRUE; 275 | protected $soft_delete_key = 'book_deleted_status'; 276 | } 277 | 278 | Now, when you make a call to any of the `get_` methods, a constraint will be added to not withdraw deleted columns: 279 | 280 | => $this->book_model->get_by('user_id', 1); 281 | -> SELECT * FROM books WHERE user_id = 1 AND deleted = 0 282 | 283 | If you'd like to include deleted columns, you can use the `with_deleted()` scope: 284 | 285 | => $this->book_model->with_deleted()->get_by('user_id', 1); 286 | -> SELECT * FROM books WHERE user_id = 1 287 | 288 | If you'd like to include only the columns that have been deleted, you can use the `only_deleted()` scope: 289 | 290 | => $this->book_model->only_deleted()->get_by('user_id', 1); 291 | -> SELECT * FROM books WHERE user_id = 1 AND deleted = 1 292 | 293 | Built-in Observers 294 | ------------------- 295 | 296 | **MY_Model** contains a few built-in observers for things I've found I've added to most of my models. 297 | 298 | The timestamps (MySQL compatible) `created_at` and `updated_at` are now available as built-in observers: 299 | 300 | class Post_model extends MY_Model 301 | { 302 | public $before_create = array( 'created_at', 'updated_at' ); 303 | public $before_update = array( 'updated_at' ); 304 | } 305 | 306 | **MY_Model** also contains serialisation observers for serialising and unserialising native PHP objects. This allows you to pass complex structures like arrays and objects into rows and have it be serialised automatically in the background. Call the `serialize` and `unserialize` observers with the column name(s) as a parameter: 307 | 308 | class Event_model extends MY_Model 309 | { 310 | public $before_create = array( 'serialize(seat_types)' ); 311 | public $before_update = array( 'serialize(seat_types)' ); 312 | public $after_get = array( 'unserialize(seat_types)' ); 313 | } 314 | 315 | Database Connection 316 | ------------------- 317 | 318 | The class will automatically use the default database connection, and even load it for you if you haven't yet. 319 | 320 | You can specify a database connection on a per-model basis by declaring the _$\_db\_group_ instance variable. This is equivalent to calling `$this->db->database($this->_db_group, TRUE)`. 321 | 322 | See ["Connecting to your Database"](http://ellislab.com/codeigniter/user-guide/database/connecting.html) for more information. 323 | 324 | ```php 325 | class Post_model extends MY_Model 326 | { 327 | public $_db_group = 'group_name'; 328 | } 329 | ``` 330 | 331 | Unit Tests 332 | ---------- 333 | 334 | MY_Model contains a robust set of unit tests to ensure that the system works as planned. 335 | 336 | Install the testing framework (PHPUnit) with Composer: 337 | 338 | $ curl -s https://getcomposer.org/installer | php 339 | $ php composer.phar install 340 | 341 | You can then run the tests using the `vendor/bin/phpunit` binary and specify the tests file: 342 | 343 | $ vendor/bin/phpunit 344 | 345 | 346 | Contributing to MY_Model 347 | ------------------------ 348 | 349 | If you find a bug or want to add a feature to MY_Model, great! In order to make it easier and quicker for me to verify and merge changes in, it would be amazing if you could follow these few basic steps: 350 | 351 | 1. Fork the project. 352 | 2. **Branch out into a new branch. `git checkout -b name_of_new_feature_or_bug`** 353 | 3. Make your feature addition or bug fix. 354 | 4. **Add tests for it. This is important so I don’t break it in a future version unintentionally.** 355 | 5. Commit. 356 | 6. Send me a pull request! 357 | 358 | 359 | Other Documentation 360 | ------------------- 361 | 362 | * My book, The CodeIgniter Handbook, talks about the techniques used in MY_Model and lots of other interesting useful stuff. [Get a copy now.](https://efendibooks.com/books/codeigniter-handbook/vol-1) 363 | * Jeff Madsen has written an excellent tutorial about the basics (and triggered me updating the documentation here). [Read it now, you lovely people.](http://www.codebyjeff.com/blog/2012/01/using-jamie-rumbelows-my_model) 364 | * Rob Allport wrote a post about MY_Model and his experiences with it. [Check it out!](http://www.web-design-talk.co.uk/493/codeigniter-base-models-rock/) 365 | * I've written a write up of the new 2.0.0 features [over at my blog, Jamie On Software.](http://jamieonsoftware.com/journal/2012/9/11/my_model-200-at-a-glance.html) 366 | 367 | Changelog 368 | --------- 369 | 370 | **Version 2.0.0** 371 | * Added support for soft deletes 372 | * Removed Composer support. Great system, CI makes it difficult to use for MY_ classes 373 | * Fixed up all problems with callbacks and consolidated into single `trigger` method 374 | * Added support for relationships 375 | * Added built-in timestamp observers 376 | * The DB connection can now be manually set with `$this->_db`, rather than relying on the `$active_group` 377 | * Callbacks can also now take parameters when setting in callback array 378 | * Added support for column serialisation 379 | * Added support for protected attributes 380 | * Added a `truncate()` method 381 | 382 | **Version 1.3.0** 383 | * Added support for array return types using `$return_type` variable and `as_array()` and `as_object()` methods 384 | * Added PHP5.3 support for the test suite 385 | * Removed the deprecated `MY_Model()` constructor 386 | * Fixed an issue with after_create callbacks (thanks [zbrox](https://github.com/zbrox)!) 387 | * Composer package will now autoload the file 388 | * Fixed the callback example by returning the given/modified data (thanks [druu](https://github.com/druu)!) 389 | * Change order of operations in `_fetch_table()` (thanks [JustinBusschau](https://github.com/JustinBusschau)!) 390 | 391 | **Version 1.2.0** 392 | * Bugfix to `update_many()` 393 | * Added getters for table name and skip validation 394 | * Fix to callback functionality (thanks [titosemi](https://github.com/titosemi)!) 395 | * Vastly improved documentation 396 | * Added a `get_next_id()` method (thanks [gbaldera](https://github.com/gbaldera)!) 397 | * Added a set of unit tests 398 | * Added support for [Composer](http://getcomposer.org/) 399 | 400 | **Version 1.0.0 - 1.1.0** 401 | * Initial Releases 402 | -------------------------------------------------------------------------------- /core/MY_Model.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | class MY_Model extends CI_Model 11 | { 12 | 13 | /* -------------------------------------------------------------- 14 | * VARIABLES 15 | * ------------------------------------------------------------ */ 16 | 17 | /** 18 | * This model's default database table. Automatically 19 | * guessed by pluralising the model name. 20 | */ 21 | protected $_table; 22 | 23 | /** 24 | * The database connection object. Will be set to the default 25 | * connection. This allows individual models to use different DBs 26 | * without overwriting CI's global $this->db connection. 27 | */ 28 | public $_database; 29 | 30 | /** 31 | * This model's default primary key or unique identifier. 32 | * Used by the get(), update() and delete() functions. 33 | */ 34 | protected $primary_key = 'id'; 35 | 36 | /** 37 | * Support for soft deletes and this model's 'deleted' key 38 | */ 39 | protected $soft_delete = FALSE; 40 | protected $soft_delete_key = 'deleted'; 41 | protected $_temporary_with_deleted = FALSE; 42 | protected $_temporary_only_deleted = FALSE; 43 | 44 | /** 45 | * The various callbacks available to the model. Each are 46 | * simple lists of method names (methods will be run on $this). 47 | */ 48 | protected $before_create = array(); 49 | protected $after_create = array(); 50 | protected $before_update = array(); 51 | protected $after_update = array(); 52 | protected $before_get = array(); 53 | protected $after_get = array(); 54 | protected $before_delete = array(); 55 | protected $after_delete = array(); 56 | 57 | protected $callback_parameters = array(); 58 | 59 | /** 60 | * Protected, non-modifiable attributes 61 | */ 62 | protected $protected_attributes = array(); 63 | 64 | /** 65 | * Relationship arrays. Use flat strings for defaults or string 66 | * => array to customise the class name and primary key 67 | */ 68 | protected $belongs_to = array(); 69 | protected $has_many = array(); 70 | 71 | protected $_with = array(); 72 | 73 | /** 74 | * An array of validation rules. This needs to be the same format 75 | * as validation rules passed to the Form_validation library. 76 | */ 77 | protected $validate = array(); 78 | 79 | /** 80 | * Optionally skip the validation. Used in conjunction with 81 | * skip_validation() to skip data validation for any future calls. 82 | */ 83 | protected $skip_validation = FALSE; 84 | 85 | /** 86 | * By default we return our results as objects. If we need to override 87 | * this, we can, or, we could use the `as_array()` and `as_object()` scopes. 88 | */ 89 | protected $return_type = 'object'; 90 | protected $_temporary_return_type = NULL; 91 | 92 | /* -------------------------------------------------------------- 93 | * GENERIC METHODS 94 | * ------------------------------------------------------------ */ 95 | 96 | /** 97 | * Initialise the model, tie into the CodeIgniter superobject and 98 | * try our best to guess the table name. 99 | */ 100 | public function __construct() 101 | { 102 | parent::__construct(); 103 | 104 | $this->load->helper('inflector'); 105 | 106 | $this->_fetch_table(); 107 | 108 | $this->_database = $this->db; 109 | 110 | array_unshift($this->before_create, 'protect_attributes'); 111 | array_unshift($this->before_update, 'protect_attributes'); 112 | 113 | $this->_temporary_return_type = $this->return_type; 114 | } 115 | 116 | /* -------------------------------------------------------------- 117 | * CRUD INTERFACE 118 | * ------------------------------------------------------------ */ 119 | 120 | /** 121 | * Fetch a single record based on the primary key. Returns an object. 122 | */ 123 | public function get($primary_value) 124 | { 125 | return $this->get_by($this->primary_key, $primary_value); 126 | } 127 | 128 | /** 129 | * Fetch a single record based on an arbitrary WHERE call. Can be 130 | * any valid value to $this->_database->where(). 131 | */ 132 | public function get_by() 133 | { 134 | $where = func_get_args(); 135 | 136 | if ($this->soft_delete && $this->_temporary_with_deleted !== TRUE) 137 | { 138 | $this->_database->where($this->soft_delete_key, (bool)$this->_temporary_only_deleted); 139 | } 140 | 141 | $this->_set_where($where); 142 | 143 | $this->trigger('before_get'); 144 | 145 | $row = $this->_database->get($this->_table) 146 | ->{$this->_return_type()}(); 147 | $this->_temporary_return_type = $this->return_type; 148 | 149 | $row = $this->trigger('after_get', $row); 150 | 151 | $this->_with = array(); 152 | return $row; 153 | } 154 | 155 | /** 156 | * Fetch an array of records based on an array of primary values. 157 | */ 158 | public function get_many($values) 159 | { 160 | $this->_database->where_in($this->primary_key, $values); 161 | 162 | return $this->get_all(); 163 | } 164 | 165 | /** 166 | * Fetch an array of records based on an arbitrary WHERE call. 167 | */ 168 | public function get_many_by() 169 | { 170 | $where = func_get_args(); 171 | 172 | $this->_set_where($where); 173 | 174 | return $this->get_all(); 175 | } 176 | 177 | /** 178 | * Fetch all the records in the table. Can be used as a generic call 179 | * to $this->_database->get() with scoped methods. 180 | */ 181 | public function get_all() 182 | { 183 | $this->trigger('before_get'); 184 | 185 | if ($this->soft_delete && $this->_temporary_with_deleted !== TRUE) 186 | { 187 | $this->_database->where($this->soft_delete_key, (bool)$this->_temporary_only_deleted); 188 | } 189 | 190 | $result = $this->_database->get($this->_table) 191 | ->{$this->_return_type(1)}(); 192 | $this->_temporary_return_type = $this->return_type; 193 | 194 | foreach ($result as $key => &$row) 195 | { 196 | $row = $this->trigger('after_get', $row, ($key == count($result) - 1)); 197 | } 198 | 199 | $this->_with = array(); 200 | return $result; 201 | } 202 | 203 | /** 204 | * Insert a new row into the table. $data should be an associative array 205 | * of data to be inserted. Returns newly created ID. 206 | */ 207 | public function insert($data, $skip_validation = FALSE) 208 | { 209 | if ($skip_validation === FALSE) 210 | { 211 | $data = $this->validate($data); 212 | } 213 | 214 | if ($data !== FALSE) 215 | { 216 | $data = $this->trigger('before_create', $data); 217 | 218 | $this->_database->insert($this->_table, $data); 219 | $insert_id = $this->_database->insert_id(); 220 | 221 | $this->trigger('after_create', $insert_id); 222 | 223 | return $insert_id; 224 | } 225 | else 226 | { 227 | return FALSE; 228 | } 229 | } 230 | 231 | /** 232 | * Insert multiple rows into the table. Returns an array of multiple IDs. 233 | */ 234 | public function insert_many($data, $skip_validation = FALSE) 235 | { 236 | $ids = array(); 237 | 238 | foreach ($data as $key => $row) 239 | { 240 | $ids[] = $this->insert($row, $skip_validation, ($key == count($data) - 1)); 241 | } 242 | 243 | return $ids; 244 | } 245 | 246 | /** 247 | * Updated a record based on the primary value. 248 | */ 249 | public function update($primary_value, $data, $skip_validation = FALSE) 250 | { 251 | $data = $this->trigger('before_update', $data); 252 | 253 | if ($skip_validation === FALSE) 254 | { 255 | $data = $this->validate($data); 256 | } 257 | 258 | if ($data !== FALSE) 259 | { 260 | $result = $this->_database->where($this->primary_key, $primary_value) 261 | ->set($data) 262 | ->update($this->_table); 263 | 264 | $this->trigger('after_update', array($data, $result)); 265 | 266 | return $result; 267 | } 268 | else 269 | { 270 | return FALSE; 271 | } 272 | } 273 | 274 | /** 275 | * Update many records, based on an array of primary values. 276 | */ 277 | public function update_many($primary_values, $data, $skip_validation = FALSE) 278 | { 279 | $data = $this->trigger('before_update', $data); 280 | 281 | if ($skip_validation === FALSE) 282 | { 283 | $data = $this->validate($data); 284 | } 285 | 286 | if ($data !== FALSE) 287 | { 288 | $result = $this->_database->where_in($this->primary_key, $primary_values) 289 | ->set($data) 290 | ->update($this->_table); 291 | 292 | $this->trigger('after_update', array($data, $result)); 293 | 294 | return $result; 295 | } 296 | else 297 | { 298 | return FALSE; 299 | } 300 | } 301 | 302 | /** 303 | * Updated a record based on an arbitrary WHERE clause. 304 | */ 305 | public function update_by() 306 | { 307 | $args = func_get_args(); 308 | $data = array_pop($args); 309 | 310 | $data = $this->trigger('before_update', $data); 311 | 312 | if ($this->validate($data) !== FALSE) 313 | { 314 | $this->_set_where($args); 315 | $result = $this->_database->set($data) 316 | ->update($this->_table); 317 | $this->trigger('after_update', array($data, $result)); 318 | 319 | return $result; 320 | } 321 | else 322 | { 323 | return FALSE; 324 | } 325 | } 326 | 327 | /** 328 | * Update all records 329 | */ 330 | public function update_all($data) 331 | { 332 | $data = $this->trigger('before_update', $data); 333 | $result = $this->_database->set($data) 334 | ->update($this->_table); 335 | $this->trigger('after_update', array($data, $result)); 336 | 337 | return $result; 338 | } 339 | 340 | /** 341 | * Delete a row from the table by the primary value 342 | */ 343 | public function delete($id) 344 | { 345 | $this->trigger('before_delete', $id); 346 | 347 | $this->_database->where($this->primary_key, $id); 348 | 349 | if ($this->soft_delete) 350 | { 351 | $result = $this->_database->update($this->_table, array( $this->soft_delete_key => TRUE )); 352 | } 353 | else 354 | { 355 | $result = $this->_database->delete($this->_table); 356 | } 357 | 358 | $this->trigger('after_delete', $result); 359 | 360 | return $result; 361 | } 362 | 363 | /** 364 | * Delete a row from the database table by an arbitrary WHERE clause 365 | */ 366 | public function delete_by() 367 | { 368 | $where = func_get_args(); 369 | 370 | $where = $this->trigger('before_delete', $where); 371 | 372 | $this->_set_where($where); 373 | 374 | 375 | if ($this->soft_delete) 376 | { 377 | $result = $this->_database->update($this->_table, array( $this->soft_delete_key => TRUE )); 378 | } 379 | else 380 | { 381 | $result = $this->_database->delete($this->_table); 382 | } 383 | 384 | $this->trigger('after_delete', $result); 385 | 386 | return $result; 387 | } 388 | 389 | /** 390 | * Delete many rows from the database table by multiple primary values 391 | */ 392 | public function delete_many($primary_values) 393 | { 394 | $primary_values = $this->trigger('before_delete', $primary_values); 395 | 396 | $this->_database->where_in($this->primary_key, $primary_values); 397 | 398 | if ($this->soft_delete) 399 | { 400 | $result = $this->_database->update($this->_table, array( $this->soft_delete_key => TRUE )); 401 | } 402 | else 403 | { 404 | $result = $this->_database->delete($this->_table); 405 | } 406 | 407 | $this->trigger('after_delete', $result); 408 | 409 | return $result; 410 | } 411 | 412 | 413 | /** 414 | * Truncates the table 415 | */ 416 | public function truncate() 417 | { 418 | $result = $this->_database->truncate($this->_table); 419 | 420 | return $result; 421 | } 422 | 423 | /* -------------------------------------------------------------- 424 | * RELATIONSHIPS 425 | * ------------------------------------------------------------ */ 426 | 427 | public function with($relationship) 428 | { 429 | $this->_with[] = $relationship; 430 | 431 | if (!in_array('relate', $this->after_get)) 432 | { 433 | $this->after_get[] = 'relate'; 434 | } 435 | 436 | return $this; 437 | } 438 | 439 | public function relate($row) 440 | { 441 | if (empty($row)) 442 | { 443 | return $row; 444 | } 445 | 446 | foreach ($this->belongs_to as $key => $value) 447 | { 448 | if (is_string($value)) 449 | { 450 | $relationship = $value; 451 | $options = array( 'primary_key' => $value . '_id', 'model' => $value . '_model' ); 452 | } 453 | else 454 | { 455 | $relationship = $key; 456 | $options = $value; 457 | } 458 | 459 | if (in_array($relationship, $this->_with)) 460 | { 461 | $this->load->model($options['model'], $relationship . '_model'); 462 | 463 | if (is_object($row)) 464 | { 465 | $row->{$relationship} = $this->{$relationship . '_model'}->get($row->{$options['primary_key']}); 466 | } 467 | else 468 | { 469 | $row[$relationship] = $this->{$relationship . '_model'}->get($row[$options['primary_key']]); 470 | } 471 | } 472 | } 473 | 474 | foreach ($this->has_many as $key => $value) 475 | { 476 | if (is_string($value)) 477 | { 478 | $relationship = $value; 479 | $options = array( 'primary_key' => singular($this->_table) . '_id', 'model' => singular($value) . '_model' ); 480 | } 481 | else 482 | { 483 | $relationship = $key; 484 | $options = $value; 485 | } 486 | 487 | if (in_array($relationship, $this->_with)) 488 | { 489 | $this->load->model($options['model'], $relationship . '_model'); 490 | 491 | if (is_object($row)) 492 | { 493 | $row->{$relationship} = $this->{$relationship . '_model'}->get_many_by($options['primary_key'], $row->{$this->primary_key}); 494 | } 495 | else 496 | { 497 | $row[$relationship] = $this->{$relationship . '_model'}->get_many_by($options['primary_key'], $row[$this->primary_key]); 498 | } 499 | } 500 | } 501 | 502 | return $row; 503 | } 504 | 505 | /* -------------------------------------------------------------- 506 | * UTILITY METHODS 507 | * ------------------------------------------------------------ */ 508 | 509 | /** 510 | * Retrieve and generate a form_dropdown friendly array 511 | */ 512 | function dropdown() 513 | { 514 | $args = func_get_args(); 515 | 516 | if(count($args) == 2) 517 | { 518 | list($key, $value) = $args; 519 | } 520 | else 521 | { 522 | $key = $this->primary_key; 523 | $value = $args[0]; 524 | } 525 | 526 | $this->trigger('before_dropdown', array( $key, $value )); 527 | 528 | if ($this->soft_delete && $this->_temporary_with_deleted !== TRUE) 529 | { 530 | $this->_database->where($this->soft_delete_key, FALSE); 531 | } 532 | 533 | $result = $this->_database->select(array($key, $value)) 534 | ->get($this->_table) 535 | ->result(); 536 | 537 | $options = array(); 538 | 539 | foreach ($result as $row) 540 | { 541 | $options[$row->{$key}] = $row->{$value}; 542 | } 543 | 544 | $options = $this->trigger('after_dropdown', $options); 545 | 546 | return $options; 547 | } 548 | 549 | /** 550 | * Fetch a count of rows based on an arbitrary WHERE call. 551 | */ 552 | public function count_by() 553 | { 554 | if ($this->soft_delete && $this->_temporary_with_deleted !== TRUE) 555 | { 556 | $this->_database->where($this->soft_delete_key, (bool)$this->_temporary_only_deleted); 557 | } 558 | 559 | $where = func_get_args(); 560 | $this->_set_where($where); 561 | 562 | return $this->_database->count_all_results($this->_table); 563 | } 564 | 565 | /** 566 | * Fetch a total count of rows, disregarding any previous conditions 567 | */ 568 | public function count_all() 569 | { 570 | if ($this->soft_delete && $this->_temporary_with_deleted !== TRUE) 571 | { 572 | $this->_database->where($this->soft_delete_key, (bool)$this->_temporary_only_deleted); 573 | } 574 | 575 | return $this->_database->count_all($this->_table); 576 | } 577 | 578 | /** 579 | * Tell the class to skip the insert validation 580 | */ 581 | public function skip_validation() 582 | { 583 | $this->skip_validation = TRUE; 584 | return $this; 585 | } 586 | 587 | /** 588 | * Get the skip validation status 589 | */ 590 | public function get_skip_validation() 591 | { 592 | return $this->skip_validation; 593 | } 594 | 595 | /** 596 | * Return the next auto increment of the table. Only tested on MySQL. 597 | */ 598 | public function get_next_id() 599 | { 600 | return (int) $this->_database->select('AUTO_INCREMENT') 601 | ->from('information_schema.TABLES') 602 | ->where('TABLE_NAME', $this->_table) 603 | ->where('TABLE_SCHEMA', $this->_database->database)->get()->row()->AUTO_INCREMENT; 604 | } 605 | 606 | /** 607 | * Getter for the table name 608 | */ 609 | public function table() 610 | { 611 | return $this->_table; 612 | } 613 | 614 | /* -------------------------------------------------------------- 615 | * GLOBAL SCOPES 616 | * ------------------------------------------------------------ */ 617 | 618 | /** 619 | * Return the next call as an array rather than an object 620 | */ 621 | public function as_array() 622 | { 623 | $this->_temporary_return_type = 'array'; 624 | return $this; 625 | } 626 | 627 | /** 628 | * Return the next call as an object rather than an array 629 | */ 630 | public function as_object() 631 | { 632 | $this->_temporary_return_type = 'object'; 633 | return $this; 634 | } 635 | 636 | /** 637 | * Don't care about soft deleted rows on the next call 638 | */ 639 | public function with_deleted() 640 | { 641 | $this->_temporary_with_deleted = TRUE; 642 | return $this; 643 | } 644 | 645 | /** 646 | * Only get deleted rows on the next call 647 | */ 648 | public function only_deleted() 649 | { 650 | $this->_temporary_only_deleted = TRUE; 651 | return $this; 652 | } 653 | 654 | /* -------------------------------------------------------------- 655 | * OBSERVERS 656 | * ------------------------------------------------------------ */ 657 | 658 | /** 659 | * MySQL DATETIME created_at and updated_at 660 | */ 661 | public function created_at($row) 662 | { 663 | if (is_object($row)) 664 | { 665 | $row->created_at = date('Y-m-d H:i:s'); 666 | } 667 | else 668 | { 669 | $row['created_at'] = date('Y-m-d H:i:s'); 670 | } 671 | 672 | return $row; 673 | } 674 | 675 | public function updated_at($row) 676 | { 677 | if (is_object($row)) 678 | { 679 | $row->updated_at = date('Y-m-d H:i:s'); 680 | } 681 | else 682 | { 683 | $row['updated_at'] = date('Y-m-d H:i:s'); 684 | } 685 | 686 | return $row; 687 | } 688 | 689 | /** 690 | * Serialises data for you automatically, allowing you to pass 691 | * through objects and let it handle the serialisation in the background 692 | */ 693 | public function serialize($row) 694 | { 695 | foreach ($this->callback_parameters as $column) 696 | { 697 | $row[$column] = serialize($row[$column]); 698 | } 699 | 700 | return $row; 701 | } 702 | 703 | public function unserialize($row) 704 | { 705 | foreach ($this->callback_parameters as $column) 706 | { 707 | if (is_array($row)) 708 | { 709 | $row[$column] = unserialize($row[$column]); 710 | } 711 | else 712 | { 713 | $row->$column = unserialize($row->$column); 714 | } 715 | } 716 | 717 | return $row; 718 | } 719 | 720 | /** 721 | * Protect attributes by removing them from $row array 722 | */ 723 | public function protect_attributes($row) 724 | { 725 | foreach ($this->protected_attributes as $attr) 726 | { 727 | if (is_object($row)) 728 | { 729 | unset($row->$attr); 730 | } 731 | else 732 | { 733 | unset($row[$attr]); 734 | } 735 | } 736 | 737 | return $row; 738 | } 739 | 740 | /* -------------------------------------------------------------- 741 | * QUERY BUILDER DIRECT ACCESS METHODS 742 | * ------------------------------------------------------------ */ 743 | 744 | /** 745 | * A wrapper to $this->_database->order_by() 746 | */ 747 | public function order_by($criteria, $order = 'ASC') 748 | { 749 | if ( is_array($criteria) ) 750 | { 751 | foreach ($criteria as $key => $value) 752 | { 753 | $this->_database->order_by($key, $value); 754 | } 755 | } 756 | else 757 | { 758 | $this->_database->order_by($criteria, $order); 759 | } 760 | return $this; 761 | } 762 | 763 | /** 764 | * A wrapper to $this->_database->limit() 765 | */ 766 | public function limit($limit, $offset = 0) 767 | { 768 | $this->_database->limit($limit, $offset); 769 | return $this; 770 | } 771 | 772 | /* -------------------------------------------------------------- 773 | * INTERNAL METHODS 774 | * ------------------------------------------------------------ */ 775 | 776 | /** 777 | * Trigger an event and call its observers. Pass through the event name 778 | * (which looks for an instance variable $this->event_name), an array of 779 | * parameters to pass through and an optional 'last in interation' boolean 780 | */ 781 | public function trigger($event, $data = FALSE, $last = TRUE) 782 | { 783 | if (isset($this->$event) && is_array($this->$event)) 784 | { 785 | foreach ($this->$event as $method) 786 | { 787 | if (strpos($method, '(')) 788 | { 789 | preg_match('/([a-zA-Z0-9\_\-]+)(\(([a-zA-Z0-9\_\-\., ]+)\))?/', $method, $matches); 790 | 791 | $method = $matches[1]; 792 | $this->callback_parameters = explode(',', $matches[3]); 793 | } 794 | 795 | $data = call_user_func_array(array($this, $method), array($data, $last)); 796 | } 797 | } 798 | 799 | return $data; 800 | } 801 | 802 | /** 803 | * Run validation on the passed data 804 | */ 805 | public function validate($data) 806 | { 807 | if($this->skip_validation) 808 | { 809 | return $data; 810 | } 811 | 812 | if(!empty($this->validate)) 813 | { 814 | foreach($data as $key => $val) 815 | { 816 | $_POST[$key] = $val; 817 | } 818 | 819 | $this->load->library('form_validation'); 820 | 821 | if(is_array($this->validate)) 822 | { 823 | $this->form_validation->set_rules($this->validate); 824 | 825 | if ($this->form_validation->run() === TRUE) 826 | { 827 | return $data; 828 | } 829 | else 830 | { 831 | return FALSE; 832 | } 833 | } 834 | else 835 | { 836 | if ($this->form_validation->run($this->validate) === TRUE) 837 | { 838 | return $data; 839 | } 840 | else 841 | { 842 | return FALSE; 843 | } 844 | } 845 | } 846 | else 847 | { 848 | return $data; 849 | } 850 | } 851 | 852 | /** 853 | * Guess the table name by pluralising the model name 854 | */ 855 | private function _fetch_table() 856 | { 857 | if ($this->_table == NULL) 858 | { 859 | $this->_table = plural(preg_replace('/(_m|_model)?$/', '', strtolower(get_class($this)))); 860 | } 861 | } 862 | 863 | /** 864 | * Guess the primary key for current table 865 | */ 866 | private function _fetch_primary_key() 867 | { 868 | if($this->primary_key == NULl) 869 | { 870 | $this->primary_key = $this->_database->query("SHOW KEYS FROM `".$this->_table."` WHERE Key_name = 'PRIMARY'")->row()->Column_name; 871 | } 872 | } 873 | 874 | /** 875 | * Set WHERE parameters, cleverly 876 | */ 877 | protected function _set_where($params) 878 | { 879 | if (count($params) == 1 && is_array($params[0])) 880 | { 881 | foreach ($params[0] as $field => $filter) 882 | { 883 | if (is_array($filter)) 884 | { 885 | $this->_database->where_in($field, $filter); 886 | } 887 | else 888 | { 889 | if (is_int($field)) 890 | { 891 | $this->_database->where($filter); 892 | } 893 | else 894 | { 895 | $this->_database->where($field, $filter); 896 | } 897 | } 898 | } 899 | } 900 | else if (count($params) == 1) 901 | { 902 | $this->_database->where($params[0]); 903 | } 904 | else if(count($params) == 2) 905 | { 906 | if (is_array($params[1])) 907 | { 908 | $this->_database->where_in($params[0], $params[1]); 909 | } 910 | else 911 | { 912 | $this->_database->where($params[0], $params[1]); 913 | } 914 | } 915 | else if(count($params) == 3) 916 | { 917 | $this->_database->where($params[0], $params[1], $params[2]); 918 | } 919 | else 920 | { 921 | if (is_array($params[1])) 922 | { 923 | $this->_database->where_in($params[0], $params[1]); 924 | } 925 | else 926 | { 927 | $this->_database->where($params[0], $params[1]); 928 | } 929 | } 930 | } 931 | 932 | /** 933 | * Return the method name for the current return type 934 | */ 935 | protected function _return_type($multi = FALSE) 936 | { 937 | $method = ($multi) ? 'result' : 'row'; 938 | return $this->_temporary_return_type == 'array' ? $method . '_array' : $method; 939 | } 940 | } 941 | -------------------------------------------------------------------------------- /tests/MY_Model_test.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | use Mockery as m; 11 | 12 | require_once 'tests/support/test_helper.php'; 13 | 14 | class MY_Model_tests extends PHPUnit_Framework_TestCase 15 | { 16 | public $model; 17 | 18 | /* -------------------------------------------------------------- 19 | * TEST INFRASTRUCTURE 20 | * ------------------------------------------------------------ */ 21 | 22 | public function setUp() 23 | { 24 | $this->model = new Record_model(); 25 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 26 | } 27 | 28 | public function tearDown() 29 | { 30 | unset($this->model); 31 | } 32 | 33 | /* -------------------------------------------------------------- 34 | * GENERIC METHODS 35 | * ------------------------------------------------------------ */ 36 | 37 | public function test_constructor_guesses_the_table_name() 38 | { 39 | $this->model = new Record_model(); 40 | 41 | $this->assertEquals($this->model->table(), 'records'); 42 | } 43 | 44 | /* -------------------------------------------------------------- 45 | * CRUD INTERFACE 46 | * ------------------------------------------------------------ */ 47 | 48 | public function test_get() 49 | { 50 | $this->model->_database->expects($this->once()) 51 | ->method('where') 52 | ->with($this->equalTo('id'), $this->equalTo(2)) 53 | ->will($this->returnValue($this->model->_database)); 54 | $this->_expect_get(); 55 | $this->model->_database->expects($this->once()) 56 | ->method('row') 57 | ->will($this->returnValue('fake_record_here')); 58 | 59 | $this->assertEquals($this->model->get(2), 'fake_record_here'); 60 | } 61 | 62 | public function test_get_by() 63 | { 64 | $this->model->_database->expects($this->once()) 65 | ->method('where') 66 | ->with($this->equalTo('some_column'), $this->equalTo('some_value')) 67 | ->will($this->returnValue($this->model->_database)); 68 | $this->_expect_get(); 69 | $this->model->_database->expects($this->once()) 70 | ->method('row') 71 | ->will($this->returnValue('fake_record_here')); 72 | 73 | $this->assertEquals($this->model->get_by('some_column', 'some_value'), 'fake_record_here'); 74 | } 75 | 76 | public function test_get_by_using_array() 77 | { 78 | $this->model->_database->expects($this->once()) 79 | ->method('where_in') 80 | ->with($this->equalTo('some_column'), array('some_value', 'some_other_value')) 81 | ->will($this->returnValue($this->model->_database)); 82 | $this->_expect_get(); 83 | $this->model->_database->expects($this->once()) 84 | ->method('row') 85 | ->will($this->returnValue('fake_record_here')); 86 | 87 | $this->assertEquals($this->model->get_by('some_column', array('some_value', 'some_other_value')), 'fake_record_here'); 88 | } 89 | 90 | public function test_get_by_using_string() 91 | { 92 | $this->model->_database->expects($this->once()) 93 | ->method('where') 94 | ->with($this->equalTo('some_column != some_value')) 95 | ->will($this->returnValue($this->model->_database)); 96 | $this->_expect_get(); 97 | $this->model->_database->expects($this->once()) 98 | ->method('row') 99 | ->will($this->returnValue('fake_record_here')); 100 | 101 | $this->assertEquals($this->model->get_by('some_column != some_value'), 'fake_record_here'); 102 | } 103 | 104 | public function test_get_by_using_mixed_params() 105 | { 106 | $where = array( 107 | 'some_column == some_value', 108 | 'some_other_column' => array('some_value', 'some_other_value'), 109 | 'another_column' => 'some_value', 110 | ); 111 | 112 | $this->model->_database->expects($this->at(0)) 113 | ->method('where') 114 | ->with('some_column == some_value') 115 | ->will($this->returnValue($this->model->_database)); 116 | 117 | $this->model->_database->expects($this->once()) 118 | ->method('where_in') 119 | ->with($this->equalTo('some_other_column'), array('some_value', 'some_other_value')) 120 | ->will($this->returnValue($this->model->_database)); 121 | 122 | $this->model->_database->expects($this->at(2)) 123 | ->method('where') 124 | ->with($this->equalTo('another_column'), $this->equalTo('some_value')) 125 | ->will($this->returnValue($this->model->_database)); 126 | 127 | $this->_expect_get(); 128 | $this->model->_database->expects($this->once()) 129 | ->method('row') 130 | ->will($this->returnValue('fake_record_here')); 131 | 132 | $this->assertEquals($this->model->get_by($where), 'fake_record_here'); 133 | } 134 | 135 | public function test_get_many() 136 | { 137 | $this->model->_database->expects($this->once()) 138 | ->method('where_in') 139 | ->with($this->equalTo('id'), $this->equalTo(array(1, 2, 3, 4, 5))) 140 | ->will($this->returnValue($this->model->_database)); 141 | $this->_expect_get(); 142 | $this->model->_database->expects($this->once()) 143 | ->method('result') 144 | ->will($this->returnValue(array('fake', 'records', 'here'))); 145 | 146 | $this->assertEquals($this->model->get_many(array(1, 2, 3, 4, 5)), array('fake', 'records', 'here')); 147 | } 148 | 149 | public function test_get_many_by() 150 | { 151 | $this->model->_database->expects($this->once()) 152 | ->method('where') 153 | ->with($this->equalTo('some_column'), $this->equalTo('some_value')) 154 | ->will($this->returnValue($this->model->_database)); 155 | $this->_expect_get(); 156 | $this->model->_database->expects($this->once()) 157 | ->method('result') 158 | ->will($this->returnValue(array('fake', 'records', 'here'))); 159 | 160 | $this->assertEquals($this->model->get_many_by('some_column', 'some_value'), array('fake', 'records', 'here')); 161 | } 162 | 163 | public function test_get_all() 164 | { 165 | $this->_expect_get(); 166 | $this->model->_database->expects($this->once()) 167 | ->method('result') 168 | ->will($this->returnValue(array('fake', 'records', 'here'))); 169 | 170 | $this->assertEquals($this->model->get_all(), array('fake', 'records', 'here')); 171 | } 172 | 173 | public function test_insert() 174 | { 175 | $this->model->_database->expects($this->once()) 176 | ->method('insert') 177 | ->with($this->equalTo('records'), $this->equalTo(array('new' => 'data'))); 178 | $this->model->_database->expects($this->any()) 179 | ->method('insert_id') 180 | ->will($this->returnValue(123)); 181 | 182 | $this->assertEquals($this->model->insert(array('new' => 'data')), 123); 183 | } 184 | 185 | public function test_insert_many() 186 | { 187 | $this->model->_database->expects($this->exactly(2)) 188 | ->method('insert') 189 | ->with($this->equalTo('records')); 190 | $this->model->_database->expects($this->any()) 191 | ->method('insert_id') 192 | ->will($this->returnValue(123)); 193 | 194 | $this->assertEquals($this->model->insert_many(array(array('new' => 'data'), array('other' => 'data'))), array(123, 123)); 195 | } 196 | 197 | public function test_update() 198 | { 199 | $this->model->_database->expects($this->once()) 200 | ->method('where') 201 | ->with($this->equalTo('id'), $this->equalTo(2)) 202 | ->will($this->returnValue($this->model->_database)); 203 | $this->model->_database->expects($this->once()) 204 | ->method('set') 205 | ->with($this->equalTo(array('new' => 'data'))) 206 | ->will($this->returnValue($this->model->_database)); 207 | $this->model->_database->expects($this->once()) 208 | ->method('update') 209 | ->with($this->equalTo('records')) 210 | ->will($this->returnValue(TRUE)); 211 | 212 | $this->assertEquals($this->model->update(2, array('new' => 'data')), TRUE); 213 | } 214 | 215 | public function test_update_many() 216 | { 217 | $this->model->_database->expects($this->once()) 218 | ->method('where_in') 219 | ->with($this->equalTo('id'), $this->equalTo(array(1, 2, 3, 4, 5))) 220 | ->will($this->returnValue($this->model->_database)); 221 | $this->model->_database->expects($this->once()) 222 | ->method('set') 223 | ->with($this->equalTo(array('new' => 'data'))) 224 | ->will($this->returnValue($this->model->_database)); 225 | $this->model->_database->expects($this->once()) 226 | ->method('update') 227 | ->with($this->equalTo('records')) 228 | ->will($this->returnValue(TRUE)); 229 | 230 | $this->assertEquals($this->model->update_many(array(1, 2, 3, 4, 5), array('new' => 'data')), TRUE); 231 | } 232 | 233 | public function test_update_by() 234 | { 235 | $this->model->_database->expects($this->once()) 236 | ->method('where') 237 | ->with($this->equalTo('some_column'), $this->equalTo('some_value')) 238 | ->will($this->returnValue($this->model->_database)); 239 | $this->model->_database->expects($this->once()) 240 | ->method('set') 241 | ->with($this->equalTo(array('new' => 'data'))) 242 | ->will($this->returnValue($this->model->_database)); 243 | $this->model->_database->expects($this->once()) 244 | ->method('update') 245 | ->with($this->equalTo('records')) 246 | ->will($this->returnValue(TRUE)); 247 | 248 | $this->assertEquals($this->model->update_by('some_column', 'some_value', array('new' => 'data')), TRUE); 249 | } 250 | 251 | public function test_update_all() 252 | { 253 | $this->model->_database->expects($this->once()) 254 | ->method('set') 255 | ->with($this->equalTo(array('new' => 'data'))) 256 | ->will($this->returnValue($this->model->_database)); 257 | $this->model->_database->expects($this->once()) 258 | ->method('update') 259 | ->with($this->equalTo('records')) 260 | ->will($this->returnValue(TRUE)); 261 | 262 | $this->assertEquals($this->model->update_all(array('new' => 'data')), TRUE); 263 | } 264 | 265 | public function test_delete() 266 | { 267 | $this->model->_database->expects($this->once()) 268 | ->method('where') 269 | ->with($this->equalTo('id'), $this->equalTo(2)) 270 | ->will($this->returnValue($this->model->_database)); 271 | $this->model->_database->expects($this->once()) 272 | ->method('delete') 273 | ->with($this->equalTo('records')) 274 | ->will($this->returnValue(TRUE)); 275 | 276 | $this->assertEquals($this->model->delete(2), TRUE); 277 | } 278 | 279 | public function test_delete_by() 280 | { 281 | $this->model->_database->expects($this->once()) 282 | ->method('where') 283 | ->with($this->equalTo('some_column'), $this->equalTo('some_value')) 284 | ->will($this->returnValue($this->model->_database)); 285 | $this->model->_database->expects($this->once()) 286 | ->method('delete') 287 | ->with($this->equalTo('records')) 288 | ->will($this->returnValue(TRUE)); 289 | 290 | $this->assertEquals($this->model->delete_by('some_column', 'some_value'), TRUE); 291 | } 292 | 293 | public function test_delete_many() 294 | { 295 | $this->model->_database->expects($this->once()) 296 | ->method('where_in') 297 | ->with($this->equalTo('id'), array(1, 2, 3, 4, 5)) 298 | ->will($this->returnValue($this->model->_database)); 299 | $this->model->_database->expects($this->once()) 300 | ->method('delete') 301 | ->with($this->equalTo('records')) 302 | ->will($this->returnValue(TRUE)); 303 | 304 | $this->assertEquals($this->model->delete_many(array(1, 2, 3, 4, 5)), TRUE); 305 | } 306 | 307 | /* -------------------------------------------------------------- 308 | * MORE CALLBACK TESTS 309 | * ------------------------------------------------------------ */ 310 | 311 | public function test_before_create_callbacks() 312 | { 313 | $this->model = new Before_callback_model(); 314 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 315 | 316 | $row = array( 'one' => 'ONE', 'two' => 'TWO' ); 317 | $expected_row = array( 'one' => 'ONE', 'two' => 'TWO', 'key' => 'Value', 'another_key' => '123 Value' ); 318 | 319 | $this->model->_database->expects($this->once()) 320 | ->method('insert') 321 | ->with($this->equalTo('records'), $this->equalTo($expected_row)); 322 | 323 | $this->model->insert($row); 324 | } 325 | 326 | public function test_after_create_callbacks() 327 | { 328 | $this->model = new After_callback_model(); 329 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 330 | 331 | $this->model->_database->expects($this->once()) 332 | ->method('insert_id') 333 | ->will($this->returnValue(10)); 334 | 335 | $self =& $this; 336 | 337 | $this->assertCallbackIsCalled(function() use ($self) 338 | { 339 | $self->model->insert(array( 'row' => 'here' )); 340 | }, 10); 341 | } 342 | 343 | public function test_before_update_callbacks() 344 | { 345 | $this->model = new Before_callback_model(); 346 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 347 | 348 | $row = array( 'one' => 'ONE', 'two' => 'TWO' ); 349 | $expected_row = array( 'one' => 'ONE', 'two' => 'TWO', 'key' => 'Value', 'another_key' => '123 Value' ); 350 | 351 | $this->model->_database->expects($this->once())->method('where')->will($this->returnValue($this->model->_database)); 352 | $this->model->_database->expects($this->once()) 353 | ->method('set') 354 | ->with($this->equalTo($expected_row)) 355 | ->will($this->returnValue($this->model->_database)); 356 | 357 | $this->model->update(1, $row); 358 | } 359 | 360 | public function test_after_update_callbacks() 361 | { 362 | $this->model = new After_callback_model(); 363 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 364 | 365 | $this->model->_database->expects($this->once())->method('where')->will($this->returnValue($this->model->_database)); 366 | $this->model->_database->expects($this->once())->method('set')->will($this->returnValue($this->model->_database)); 367 | $this->model->_database->expects($this->once())->method('update')->will($this->returnValue(TRUE)); 368 | 369 | $self =& $this; 370 | 371 | $this->assertCallbackIsCalled(function() use ($self) 372 | { 373 | $self->model->update(1, array( 'row' => 'here' )); 374 | }, array( array( 'row' => 'here' ), true )); 375 | } 376 | 377 | public function test_before_get_callbacks() 378 | { 379 | $this->model = new Before_callback_model(); 380 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 381 | 382 | $self =& $this; 383 | 384 | $this->assertCallbackIsCalled(function() use ($self) 385 | { 386 | $self->model->get(1); 387 | }, NULL); 388 | } 389 | 390 | public function test_after_get_callbacks() 391 | { 392 | $this->model = new After_callback_model(); 393 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 394 | 395 | $db_row = array( 'one' => 'ONE', 'two' => 'TWO' ); 396 | $expected_row = array( 'one' => 'ONE', 'two' => 'TWO', 'key' => 'Value', 'another_key' => '123 Value' ); 397 | 398 | $this->model->_database->expects($this->once())->method('where')->will($this->returnValue($this->model->_database)); 399 | $this->model->_database->expects($this->once())->method('get')->will($this->returnValue($this->model->_database)); 400 | $this->model->_database->expects($this->once())->method('row')->will($this->returnValue($db_row)); 401 | 402 | $this->assertEquals($expected_row, $this->model->get(1)); 403 | } 404 | 405 | public function test_before_delete_callbacks() 406 | { 407 | $this->model = new Before_callback_model(); 408 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 409 | 410 | $self =& $this; 411 | 412 | $this->assertCallbackIsCalled(function() use ($self) 413 | { 414 | $self->model->delete(12); 415 | }, 12); 416 | } 417 | 418 | public function test_after_delete_callbacks() 419 | { 420 | $this->model = new After_callback_model(); 421 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 422 | 423 | $this->model->_database->expects($this->once())->method('where')->will($this->returnValue($this->model->_database)); 424 | $this->model->_database->expects($this->once())->method('delete')->will($this->returnValue(TRUE)); 425 | 426 | $self =& $this; 427 | 428 | $this->assertCallbackIsCalled(function() use ($self) 429 | { 430 | $self->model->delete(9); 431 | }, TRUE); 432 | } 433 | 434 | public function test_callbacks_support_parameters() 435 | { 436 | $this->model = new Callback_parameter_model(); 437 | 438 | $self =& $this; 439 | $callback_parameters = array( 440 | 'some_param', 'another_param' 441 | ); 442 | 443 | $this->assertCallbackIsCalled(function() use ($self) 444 | { 445 | $self->model->some_method(); 446 | }, $callback_parameters); 447 | } 448 | 449 | /** 450 | * Callbacks, if called in an array, should receive a "last" boolean 451 | * when they're in the last iteration of triggering - the last row in a result 452 | * array, for instance - for clearing things up 453 | */ 454 | public function test_callbacks_in_iteration_have_last_variable() 455 | { 456 | // stub 457 | } 458 | 459 | /* -------------------------------------------------------------- 460 | * PROTECTED ATTRIBUTES 461 | * ------------------------------------------------------------ */ 462 | 463 | public function test_protected_attributes() 464 | { 465 | $this->model = new Protected_attributes_model(); 466 | 467 | $author = array( 468 | 'id' => 123, 469 | 'hash' => 'dlkadflsdasdsadsds', 470 | 'title' => 'A new post' 471 | ); 472 | $author_obj = (object)$author; 473 | 474 | $author = $this->model->protect_attributes($author); 475 | $author_obj = $this->model->protect_attributes($author_obj); 476 | 477 | $this->assertFalse(isset($author['id'])); 478 | $this->assertFalse(isset($author['hash'])); 479 | $this->assertFalse(isset($author_obj->id)); 480 | $this->assertFalse(isset($author_obj->hash)); 481 | } 482 | 483 | /* -------------------------------------------------------------- 484 | * RELATIONSHIPS 485 | * ------------------------------------------------------------ */ 486 | 487 | public function test_belongs_to() 488 | { 489 | $object = (object)array( 'title' => 'A Post', 'created_at' => time(), 'author_id' => 43 ); 490 | $author_object = (object)array( 'id' => 43, 'name' => 'Jamie', 'age' => 20 ); 491 | $expected_object = (object)array( 'title' => 'A Post', 'created_at' => time(), 'author_id' => 43, 'author' => $author_object ); 492 | 493 | $self =& $this; 494 | 495 | $this->model = new Belongs_to_model(); 496 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 497 | $this->model->load = $this->getMock('MY_Model_Mock_Loader'); 498 | 499 | $author_model = new Author_model(); 500 | $author_model->_database = $this->getMockBuilder('MY_Model_Mock_DB')->setMockClassName('Other_Mock_MY_Model_Mock_DB')->getMock(); 501 | 502 | $author_model->_database->expects($this->once())->method('where')->will($this->returnValue($author_model->_database)); 503 | $author_model->_database->expects($this->once())->method('get')->will($this->returnValue($author_model->_database)); 504 | 505 | $this->model->_database->expects($this->once())->method('where')->will($this->returnValue($this->model->_database)); 506 | $this->model->_database->expects($this->once())->method('get')->will($this->returnValue($this->model->_database)); 507 | 508 | $this->model->_database->expects($this->once())->method('row')->will($this->returnValue($object)); 509 | $author_model->_database->expects($this->once())->method('row')->will($this->returnValue($author_object)); 510 | 511 | $this->model->load->expects($this->once())->method('model')->with('author_model', 'author_model') 512 | ->will($this->returnCallback(function() use ($self, $author_model){ 513 | $self->model->author_model = $author_model; 514 | })); 515 | 516 | $this->assertEquals($expected_object, $this->model->with('author')->get(1)); 517 | } 518 | 519 | public function test_has_many() 520 | { 521 | $object = (object)array( 'id' => 1, 'title' => 'A Post', 'created_at' => time(), 'author_id' => 43 ); 522 | 523 | $comment_object = (object)array( 'id' => 1, 'comment' => 'A comment' ); 524 | $comment_object_2 = (object)array( 'id' => 2, 'comment' => 'Another comment' ); 525 | 526 | $expected_object = (object)array( 'id' => 1, 'title' => 'A Post', 'created_at' => time(), 'author_id' => 43, 527 | 'comments' => array( $comment_object, $comment_object_2 ) ); 528 | 529 | $self =& $this; 530 | 531 | $this->model = new Belongs_to_model(); 532 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 533 | $this->model->load = $this->getMock('MY_Model_Mock_Loader'); 534 | 535 | $comment_model = new Author_model(); 536 | $comment_model->_database = $this->getMockBuilder('MY_Model_Mock_DB')->setMockClassName('Another_Mock_MY_Model_Mock_DB')->getMock(); 537 | 538 | $comment_model->_database->expects($this->once())->method('where')->with('comment_id', 1)->will($this->returnValue($comment_model->_database)); 539 | $comment_model->_database->expects($this->once())->method('get')->will($this->returnValue($comment_model->_database)); 540 | 541 | $this->model->_database->expects($this->once())->method('where')->will($this->returnValue($this->model->_database)); 542 | $this->model->_database->expects($this->once())->method('get')->will($this->returnValue($this->model->_database)); 543 | 544 | $this->model->_database->expects($this->once())->method('row')->will($this->returnValue($object)); 545 | $comment_model->_database->expects($this->once())->method('result')->will($this->returnValue(array( $comment_object, $comment_object_2 ))); 546 | 547 | $this->model->load->expects($this->once())->method('model')->with('comment_model', 'comments_model') 548 | ->will($this->returnCallback(function() use ($self, $comment_model){ 549 | $self->model->comments_model = $comment_model; 550 | })); 551 | 552 | $this->assertEquals($expected_object, $this->model->with('comments')->get(1)); 553 | } 554 | 555 | public function test_relate_works_with_objects_and_arrays() 556 | { 557 | $data = array( 'name' => 'Jamie', 'author_id' => 1 ); 558 | $author = 'related object'; 559 | 560 | $this->model = new Belongs_to_model(); 561 | $this->model->author_model = m::mock(new Author_model()); 562 | $this->model->author_model->shouldReceive('get') 563 | ->andReturn($author); 564 | 565 | $obj = $this->model->with('author')->relate((object)$data); 566 | $arr = $this->model->with('author')->relate($data); 567 | 568 | $this->assertInternalType('object', $obj); 569 | $this->assertInternalType('array', $arr); 570 | $this->assertTrue(isset($obj->author)); 571 | $this->assertTrue(isset($arr['author'])); 572 | $this->assertEquals($author, $obj->author); 573 | $this->assertEquals($author, $arr['author']); 574 | } 575 | 576 | /* -------------------------------------------------------------- 577 | * VALIDATION 578 | * ------------------------------------------------------------ */ 579 | 580 | public function test_validate_correctly_returns_the_data_on_success_and_FALSE_on_failure() 581 | { 582 | $this->model = $this->_validatable_model(); 583 | $data = array( 'name' => 'Jamie', 'sexyness' => 'loads' ); 584 | 585 | $this->assertEquals($this->model->validate($data), $data); 586 | 587 | $this->model = $this->_validatable_model(FALSE); 588 | $this->assertEquals($this->model->validate($data), FALSE); 589 | } 590 | 591 | public function test_skip_validation() 592 | { 593 | $ret = $this->model->skip_validation(); 594 | 595 | $this->assertEquals($ret, $this->model); 596 | $this->assertEquals($this->model->get_skip_validation(), TRUE); 597 | } 598 | 599 | protected function _validatable_model($validate_pass_or_fail = TRUE) 600 | { 601 | $model = new Validated_model(); 602 | $model->form_validation = m::mock('form_validation_class'); 603 | $model->form_validation->shouldIgnoreMissing(); 604 | $model->form_validation->shouldReceive('run') 605 | ->andReturn($validate_pass_or_fail); 606 | 607 | return $model; 608 | } 609 | 610 | /* -------------------------------------------------------------- 611 | * SOFT DELETE 612 | * ------------------------------------------------------------ */ 613 | 614 | public function test_soft_delete() 615 | { 616 | $this->model = new Soft_delete_model(); 617 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 618 | 619 | $this->model->_database->expects($this->once()) 620 | ->method('where') 621 | ->with($this->equalTo('id'), $this->equalTo(2)) 622 | ->will($this->returnValue($this->model->_database)); 623 | $this->model->_database->expects($this->once()) 624 | ->method('update') 625 | ->with($this->equalTo('records'), $this->equalTo(array( 'deleted' => TRUE ))) 626 | ->will($this->returnValue(TRUE)); 627 | 628 | $this->assertEquals($this->model->delete(2), TRUE); 629 | } 630 | 631 | public function test_soft_delete_custom_key() 632 | { 633 | $this->model = new Soft_delete_model('record_deleted'); 634 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 635 | 636 | $this->model->_database->expects($this->once()) 637 | ->method('where') 638 | ->with($this->equalTo('id'), $this->equalTo(2)) 639 | ->will($this->returnValue($this->model->_database)); 640 | $this->model->_database->expects($this->once()) 641 | ->method('update') 642 | ->with($this->equalTo('records'), $this->equalTo(array( 'record_deleted' => TRUE ))) 643 | ->will($this->returnValue(TRUE)); 644 | 645 | $this->assertEquals($this->model->delete(2), TRUE); 646 | } 647 | 648 | public function test_soft_delete_by() 649 | { 650 | $this->model = new Soft_delete_model(); 651 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 652 | 653 | $this->model->_database->expects($this->once()) 654 | ->method('where') 655 | ->with($this->equalTo('key'), $this->equalTo('value')) 656 | ->will($this->returnValue($this->model->_database)); 657 | $this->model->_database->expects($this->once()) 658 | ->method('update') 659 | ->with($this->equalTo('records'), $this->equalTo(array( 'deleted' => TRUE ))) 660 | ->will($this->returnValue(TRUE)); 661 | 662 | $this->assertEquals($this->model->delete_by('key', 'value'), TRUE); 663 | } 664 | 665 | public function test_soft_delete_many() 666 | { 667 | $this->model = new Soft_delete_model(); 668 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 669 | 670 | $this->model->_database->expects($this->once()) 671 | ->method('where_in') 672 | ->with($this->equalTo('id'), $this->equalTo(array(2, 4, 6))) 673 | ->will($this->returnValue($this->model->_database)); 674 | $this->model->_database->expects($this->once()) 675 | ->method('update') 676 | ->with($this->equalTo('records'), $this->equalTo(array( 'deleted' => TRUE ))) 677 | ->will($this->returnValue(TRUE)); 678 | 679 | $this->assertEquals($this->model->delete_many(array(2, 4, 6)), TRUE); 680 | } 681 | 682 | public function test_soft_delete_get() 683 | { 684 | $this->model = new Soft_delete_model(); 685 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 686 | 687 | $this->model->_database->expects($this->at(0)) 688 | ->method('where') 689 | ->with($this->equalTo('deleted'), $this->equalTo(FALSE)) 690 | ->will($this->returnValue($this->model->_database)); 691 | $this->model->_database->expects($this->at(1)) 692 | ->method('where') 693 | ->with($this->equalTo('id'), $this->equalTo(2)) 694 | ->will($this->returnValue($this->model->_database)); 695 | $this->_expect_get(); 696 | $this->model->_database->expects($this->once()) 697 | ->method('row') 698 | ->will($this->returnValue('fake_record_here')); 699 | 700 | $this->assertEquals($this->model->get(2), 'fake_record_here'); 701 | } 702 | 703 | public function test_soft_delete_dropdown() 704 | { 705 | $this->model = new Soft_delete_model(); 706 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 707 | 708 | $fake_row_1 = array( 'id' => 1, 'name' => 'Jamie' ); 709 | $fake_row_2 = array( 'id' => 2, 'name' => 'Laura' ); 710 | $fake_results = array( (object)$fake_row_1, (object)$fake_row_2 ); 711 | 712 | $this->model->_database->expects($this->at(0)) 713 | ->method('where') 714 | ->with($this->equalTo('deleted'), $this->equalTo(FALSE)) 715 | ->will($this->returnValue($this->model->_database)); 716 | 717 | $this->model->_database->expects($this->once()) 718 | ->method('select') 719 | ->with($this->equalTo(array('id', 'name'))) 720 | ->will($this->returnValue($this->model->_database)); 721 | $this->_expect_get(); 722 | $this->model->_database->expects($this->any()) 723 | ->method('result') 724 | ->will($this->returnValue($fake_results)); 725 | 726 | $this->model->dropdown('name'); 727 | } 728 | 729 | public function test_with_deleted() 730 | { 731 | $this->model = new Soft_delete_model(); 732 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 733 | 734 | $this->model->_database->expects($this->exactly(1)) 735 | ->method('where') 736 | ->with($this->equalTo('id'), $this->equalTo(2)) 737 | ->will($this->returnValue($this->model->_database)); 738 | $this->_expect_get(); 739 | $this->model->_database->expects($this->once()) 740 | ->method('row') 741 | ->will($this->returnValue('fake_record_here')); 742 | 743 | $this->assertEquals($this->model->with_deleted()->get(2), 'fake_record_here'); 744 | } 745 | 746 | public function test_only_deleted() 747 | { 748 | $this->model = new Soft_delete_model(); 749 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 750 | 751 | $this->model->_database->expects($this->once()) 752 | ->method('where') 753 | ->with($this->equalTo('deleted'), $this->equalTo(TRUE)) 754 | ->will($this->returnValue($this->model->_database)); 755 | $this->_expect_get(); 756 | $this->model->_database->expects($this->once()) 757 | ->method('result') 758 | ->will($this->returnValue(array('fake_record_here'))); 759 | 760 | $this->assertEquals($this->model->only_deleted()->get_all(), array('fake_record_here')); 761 | } 762 | 763 | /* -------------------------------------------------------------- 764 | * CALLBACKS 765 | * ------------------------------------------------------------ */ 766 | 767 | public function test_serialize() 768 | { 769 | $this->model = new Serialised_data_model(); 770 | $this->model->_database = $this->getMock('MY_Model_Mock_DB'); 771 | 772 | $data = array( 'name' => 'Jamie', 'awesomeness_level' => 1000000 ); 773 | 774 | $this->model->_database->expects($this->exactly(1)) 775 | ->method('insert') 776 | ->with($this->equalTo('records'), $this->equalTo(array( 'data' => serialize($data) ))); 777 | 778 | $this->model->insert(array( 'data' => $data )); 779 | } 780 | 781 | public function test_timestamps() 782 | { 783 | $this->model = new Record_model(); 784 | 785 | $data = array( 'name' => 'Jamie' ); 786 | $obj = (object)array( 'name' => 'Jamie' ); 787 | 788 | $data = $this->model->created_at($data); 789 | $obj = $this->model->created_at($obj); 790 | $data = $this->model->updated_at($data); 791 | $obj = $this->model->updated_at($obj); 792 | 793 | $this->assertTrue(isset($data['created_at'])); 794 | $this->assertRegExp("/[0-9]{4}\-[0-9]{2}\-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/", $data['created_at']); 795 | $this->assertTrue(isset($obj->created_at)); 796 | $this->assertRegExp("/[0-9]{4}\-[0-9]{2}\-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/", $obj->created_at); 797 | $this->assertTrue(isset($data['updated_at'])); 798 | $this->assertRegExp("/[0-9]{4}\-[0-9]{2}\-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/", $data['updated_at']); 799 | $this->assertTrue(isset($obj->updated_at)); 800 | $this->assertRegExp("/[0-9]{4}\-[0-9]{2}\-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/", $obj->updated_at); 801 | } 802 | 803 | /* -------------------------------------------------------------- 804 | * UTILITY METHODS 805 | * ------------------------------------------------------------ */ 806 | 807 | public function test_dropdown() 808 | { 809 | $fake_row_1 = array( 'id' => 1, 'name' => 'Jamie' ); 810 | $fake_row_2 = array( 'id' => 2, 'name' => 'Laura' ); 811 | 812 | $fake_results = array( (object)$fake_row_1, (object)$fake_row_2 ); 813 | 814 | $this->model->_database->expects($this->once()) 815 | ->method('select') 816 | ->with($this->equalTo(array('id', 'name'))) 817 | ->will($this->returnValue($this->model->_database)); 818 | $this->_expect_get(); 819 | $this->model->_database->expects($this->any()) 820 | ->method('result') 821 | ->will($this->returnValue($fake_results)); 822 | 823 | $this->assertEquals($this->model->dropdown('name'), array( 1 => 'Jamie', 2 => 'Laura' )); 824 | } 825 | 826 | public function test_count_by() 827 | { 828 | $this->model->_database->expects($this->once()) 829 | ->method('where') 830 | ->with($this->equalTo('some_column'), $this->equalTo('some_value')) 831 | ->will($this->returnValue($this->model->_database)); 832 | $this->model->_database->expects($this->once()) 833 | ->method('count_all_results') 834 | ->will($this->returnValue(5)); 835 | 836 | $this->assertEquals($this->model->count_by('some_column', 'some_value'), 5); 837 | } 838 | 839 | public function test_count_all() 840 | { 841 | $this->model->_database->expects($this->once()) 842 | ->method('count_all') 843 | ->with($this->equalTo('records')) 844 | ->will($this->returnValue(200)); 845 | $this->assertEquals($this->model->count_all(), 200); 846 | } 847 | 848 | public function test_get_next_id() 849 | { 850 | $this->model->_database->database = 'some_database_name'; 851 | 852 | $this->model->_database->expects($this->once()) 853 | ->method('select') 854 | ->with($this->equalTo('AUTO_INCREMENT')) 855 | ->will($this->returnValue($this->model->_database)); 856 | $this->model->_database->expects($this->once()) 857 | ->method('from') 858 | ->with($this->equalTo('information_schema.TABLES')) 859 | ->will($this->returnValue($this->model->_database)); 860 | $this->model->_database->expects($this->any()) 861 | ->method('where') 862 | ->will($this->returnValue($this->model->_database)); 863 | $this->model->_database->expects($this->once()) 864 | ->method('get') 865 | ->will($this->returnValue($this->model->_database)); 866 | $this->model->_database->expects($this->once()) 867 | ->method('row') 868 | ->will($this->returnValue((object)array( 'AUTO_INCREMENT' => 250 ))); 869 | 870 | $this->assertEquals($this->model->get_next_id(), 250); 871 | } 872 | 873 | public function test_as_array() 874 | { 875 | $this->model->_database->expects($this->once()) 876 | ->method('where') 877 | ->with($this->equalTo('id'), $this->equalTo(2)) 878 | ->will($this->returnValue($this->model->_database)); 879 | $this->_expect_get(); 880 | $this->model->_database->expects($this->once()) 881 | ->method('row_array') 882 | ->will($this->returnValue('fake_record_here')); 883 | 884 | $this->assertEquals($this->model->as_array()->get(2), 'fake_record_here'); 885 | } 886 | 887 | /* -------------------------------------------------------------- 888 | * QUERY BUILDER DIRECT ACCESS METHODS 889 | * ------------------------------------------------------------ */ 890 | 891 | public function test_order_by_regular() 892 | { 893 | $this->model->_database->expects($this->once()) 894 | ->method('order_by') 895 | ->with($this->equalTo('some_column'), $this->equalTo('DESC')); 896 | 897 | $this->assertEquals($this->model->order_by('some_column', 'DESC'), $this->model); 898 | } 899 | 900 | public function test_order_by_array() 901 | { 902 | $this->model->_database->expects($this->once()) 903 | ->method('order_by') 904 | ->with($this->equalTo('some_column'), $this->equalTo('ASC')); 905 | 906 | $this->assertEquals($this->model->order_by(array('some_column' => 'ASC')), $this->model); 907 | } 908 | 909 | public function test_limit() 910 | { 911 | $this->model->_database->expects($this->once()) 912 | ->method('limit') 913 | ->with($this->equalTo(10), $this->equalTo(5)); 914 | 915 | $this->assertEquals($this->model->limit(10, 5), $this->model); 916 | } 917 | 918 | public function test_truncate() 919 | { 920 | $this->model->_database->expects($this->once()) 921 | ->method('truncate') 922 | ->with($this->equalTo('records')); 923 | 924 | $this->model->truncate(); 925 | } 926 | 927 | /* -------------------------------------------------------------- 928 | * TEST UTILITIES 929 | * ------------------------------------------------------------ */ 930 | 931 | protected function _expect_get() 932 | { 933 | $this->model->_database->expects($this->once()) 934 | ->method('get') 935 | ->with($this->equalTo('records')) 936 | ->will($this->returnValue($this->model->_database)); 937 | } 938 | 939 | /* -------------------------------------------------------------- 940 | * CUSTOM ASSERTIONS 941 | * ------------------------------------------------------------ */ 942 | 943 | public function assertCallbackIsCalled($method, $params = null) 944 | { 945 | try 946 | { 947 | $method(); 948 | $this->fail('Callback wasn\'t called'); 949 | } 950 | catch (Callback_Test_Exception $e) 951 | { 952 | if (!is_null($params)) 953 | { 954 | $this->assertEquals($e->passed_object, $params); 955 | } 956 | } 957 | } 958 | } --------------------------------------------------------------------------------