diff --git a/README.md b/README.md index 38fc601..ffb0876 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ $this->post->insert(array( $this->post->update(1, array( 'status' => 'closed' )); $this->post->delete(1); + +$this->post->undelete(1); // works only if soft delete is enabled (see below) ``` Installation/Usage @@ -80,6 +82,8 @@ The full list of observers are as follows: * $after_get * $before_delete * $after_delete +* $before_undelete +* $after_undelete 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: @@ -110,6 +114,10 @@ Observers can also take parameters in their name, much like CodeIgniter's Form V return $row; } +If you need the primary key value in an observer, you can use `get_primary_value()` utility method. This works only for single-row queries (eg: `get`, `create`, `update`, `delete`). On multi-row queries, it will return FALSE (eg: `get_all`, `delete_by`). + +You can temporarly disable callbacks (eg. to get raw MySQL data) using `without_callbacks()` method. + Validation ---------- @@ -210,6 +218,16 @@ The related data will be embedded in the returned value from `get`: echo $message; } +You can also access a deeper level of related data by specifying a second parameter to the `with()` method: + + $post = $this->post_model->with('author', 'country') + ->with('comments') + ->get(1); + +Will allow you to use: + + echo $post->author->country->name; + Separate queries will be run to select the data, so where performance is important, a separate JOIN and SELECT call is recommended. The primary key can also be configured. For _belongs\_to_ calls, the related key is on the current object, not the foreign one. Pseudocode: @@ -228,6 +246,11 @@ To change this, use the `primary_key` value when configuring: public $has_many = array( 'comments' => array( 'primary_key' => 'parent_post_id' ) ); } +You can also create a more complex join using `join()` method which works exactly as its ActiveRecord counterpart. + + $post = $this->post_model->join('author', 'author_id = post_author', 'left') + ->get(1); + Arrays vs Objects ----------------- @@ -273,21 +296,25 @@ By default, MY_Model expects a `TINYINT` or `INT` column named `deleted`. If you protected $soft_delete_key = 'book_deleted_status'; } -Now, when you make a call to any of the `get_` methods, a constraint will be added to not withdraw deleted columns: +Now, when you make a call to any of the `get_` methods, a constraint will be added to not withdraw deleted rows: => $this->book_model->get_by('user_id', 1); -> SELECT * FROM books WHERE user_id = 1 AND deleted = 0 -If you'd like to include deleted columns, you can use the `with_deleted()` scope: +If you'd like to include deleted rows, you can use the `with_deleted()` scope: => $this->book_model->with_deleted()->get_by('user_id', 1); -> SELECT * FROM books WHERE user_id = 1 -If you'd like to include only the columns that have been deleted, you can use the `only_deleted()` scope: +If you'd like to include only the rows that have been deleted, you can use the `only_deleted()` scope: => $this->book_model->only_deleted()->get_by('user_id', 1); -> SELECT * FROM books WHERE user_id = 1 AND deleted = 1 +If you'd like to undelete a previously delete entry, you can use the `undelete()` method: + + => $this->book_model->undelete(1); + Built-in Observers ------------------- @@ -310,6 +337,30 @@ The timestamps (MySQL compatible) `created_at` and `updated_at` are now availabl public $after_get = array( 'unserialize(seat_types)' ); } +Common restrictions +------------------- + +**MY_Model** contains an easy way to restrict all its results. Say you have an admin account which can see everything and user accounts which can see only their results, you can use the following codein your controller: + + class Books extends CI_Controller { + + function __construct() { + parent::__construct(); + + $this->load->model('book_model'); + + if (!$user_is_admin) + { + $this->book_model->set_restriction('book_owner = '.(int)$user_id); + } + } + +Don't forget to assign the correct value for `book_owner` when you create a new record, else the user won't be able to see its book. + +If you'd like to include restricted rows, you can use the `without_restriction()` scope: + + => $this->book_model->without_restriction()->get_by('user_id', 1); + Database Connection ------------------- @@ -365,6 +416,10 @@ Other Documentation Changelog --------- +**Version 2.1.0** +* Added support for undeletes when using soft deletes (thanks [julienmru](https://github.com/julienmru)!) +* Added support for restricting viewable results (thanks [julienmru](https://github.com/julienmru)!) + **Version 2.0.0** * Added support for soft deletes * Removed Composer support. Great system, CI makes it difficult to use for MY_ classes diff --git a/core/MY_Model.php b/core/MY_Model.php index 05add98..31b8ba4 100644 --- a/core/MY_Model.php +++ b/core/MY_Model.php @@ -32,6 +32,7 @@ class MY_Model extends CI_Model * Used by the get(), update() and delete() functions. */ protected $primary_key = 'id'; + protected $primary_value = FALSE; /** * Support for soft deletes and this model's 'deleted' key @@ -41,6 +42,12 @@ class MY_Model extends CI_Model protected $_temporary_with_deleted = FALSE; protected $_temporary_only_deleted = FALSE; + /** + * Support for custom restrictions + */ + protected $restriction = FALSE; + protected $_temporary_without_restriction = FALSE; + /** * The various callbacks available to the model. Each are * simple lists of method names (methods will be run on $this). @@ -54,6 +61,8 @@ class MY_Model extends CI_Model protected $before_delete = array(); protected $after_delete = array(); + protected $_temporary_without_triggers = FALSE; + protected $callback_parameters = array(); /** @@ -69,6 +78,7 @@ class MY_Model extends CI_Model protected $has_many = array(); protected $_with = array(); + protected $_subwith = array(); /** * An array of validation rules. This needs to be the same format @@ -122,7 +132,9 @@ public function __construct() */ public function get($primary_value) { - return $this->get_by($this->primary_key, $primary_value); + $this->primary_value = $primary_value; + + return $this->get_by($this->primary_key, $primary_value); } /** @@ -133,12 +145,11 @@ public function get_by() { $where = func_get_args(); - if ($this->soft_delete && $this->_temporary_with_deleted !== TRUE) - { - $this->_database->where($this->soft_delete_key, (bool)$this->_temporary_only_deleted); - } + $this->add_restrictions(); - $this->_set_where($where); + $this->primary_value = FALSE; + + $this->_set_where($where); $this->trigger('before_get'); @@ -180,20 +191,22 @@ public function get_many_by() */ public function get_all() { + + $this->primary_value = FALSE; + $this->trigger('before_get'); - if ($this->soft_delete && $this->_temporary_with_deleted !== TRUE) - { - $this->_database->where($this->soft_delete_key, (bool)$this->_temporary_only_deleted); - } + $this->add_restrictions(); $result = $this->_database->get($this->_table) ->{$this->_return_type(1)}(); $this->_temporary_return_type = $this->return_type; + $last_key = count($result) - 1; + foreach ($result as $key => &$row) { - $row = $this->trigger('after_get', $row, ($key == count($result) - 1)); + $row = $this->trigger('after_get', $row, ($key == $last_key)); } $this->_with = array(); @@ -213,11 +226,16 @@ public function insert($data, $skip_validation = FALSE) if ($data !== FALSE) { + + $this->primary_value = FALSE; + $data = $this->trigger('before_create', $data); $this->_database->insert($this->_table, $data); $insert_id = $this->_database->insert_id(); + $this->primary_value = $insert_id; + $this->trigger('after_create', $insert_id); return $insert_id; @@ -237,7 +255,7 @@ public function insert_many($data, $skip_validation = FALSE) foreach ($data as $key => $row) { - $ids[] = $this->insert($row, $skip_validation, ($key == count($data) - 1)); + $ids[] = $this->insert($row, $skip_validation); } return $ids; @@ -248,8 +266,12 @@ public function insert_many($data, $skip_validation = FALSE) */ public function update($primary_value, $data, $skip_validation = FALSE) { + $this->primary_value = $primary_value; + $data = $this->trigger('before_update', $data); + $this->add_restrictions(); + if ($skip_validation === FALSE) { $data = $this->validate($data); @@ -276,8 +298,13 @@ public function update($primary_value, $data, $skip_validation = FALSE) */ public function update_many($primary_values, $data, $skip_validation = FALSE) { + + $this->primary_value = FALSE; + $data = $this->trigger('before_update', $data); + $this->add_restrictions(); + if ($skip_validation === FALSE) { $data = $this->validate($data); @@ -307,6 +334,10 @@ public function update_by() $args = func_get_args(); $data = array_pop($args); + $this->add_restrictions(); + + $this->primary_value = FALSE; + $data = $this->trigger('before_update', $data); if ($this->validate($data) !== FALSE) @@ -329,6 +360,11 @@ public function update_by() */ public function update_all($data) { + + $this->add_restrictions(); + + $this->primary_value = FALSE; + $data = $this->trigger('before_update', $data); $result = $this->_database->set($data) ->update($this->_table); @@ -342,6 +378,11 @@ public function update_all($data) */ public function delete($id) { + + $this->primary_value = $id; + + $this->add_restrictions(FALSE); + $this->trigger('before_delete', $id); $this->_database->where($this->primary_key, $id); @@ -360,14 +401,47 @@ public function delete($id) return $result; } + /** + * Undelete a row from the table by the primary value (only if soft delete is enabled) + */ + public function undelete($id) + { + + $this->add_restrictions(FALSE); + + $this->primary_value = $id; + + $this->trigger('before_undelete', $id); + + $this->_database->where($this->primary_key, $id); + + if ($this->soft_delete) + { + $result = $this->_database->update($this->_table, array( $this->soft_delete_key => FALSE )); + } + else + { + $result = FALSE; + } + + $this->trigger('after_undelete', $result); + + return $result; + } + /** * Delete a row from the database table by an arbitrary WHERE clause */ public function delete_by() { + + $this->add_restrictions(FALSE); + $where = func_get_args(); - $where = $this->trigger('before_delete', $where); + $this->primary_value = FALSE; + + $where = $this->trigger('before_delete', $where); $this->_set_where($where); @@ -391,6 +465,11 @@ public function delete_by() */ public function delete_many($primary_values) { + + $this->add_restrictions(FALSE); + + $this->primary_value = FALSE; + $primary_values = $this->trigger('before_delete', $primary_values); $this->_database->where_in($this->primary_key, $primary_values); @@ -415,8 +494,17 @@ public function delete_many($primary_values) */ public function truncate() { - $result = $this->_database->truncate($this->_table); - + if ($this->restriction) + { + $this->add_restrictions(FALSE); + + $result = $this->_database->delete_by("1 = 1"); + } + else + { + $result = $this->_database->truncate($this->_table); + } + return $result; } @@ -424,9 +512,10 @@ public function truncate() * RELATIONSHIPS * ------------------------------------------------------------ */ - public function with($relationship) + public function with($relationship, $subrelationship = FALSE) { $this->_with[] = $relationship; + if ($subrelationship !== FALSE ) $this->_subwith[$relationship] = $subrelationship; if (!in_array('relate', $this->after_get)) { @@ -438,9 +527,9 @@ public function with($relationship) public function relate($row) { - if (empty($row)) + if (empty($row)) { - return $row; + return $row; } foreach ($this->belongs_to as $key => $value) @@ -462,11 +551,13 @@ public function relate($row) if (is_object($row)) { - $row->{$relationship} = $this->{$relationship . '_model'}->get($row->{$options['primary_key']}); + if (isset($this->_subwith[$relationship]) && $this->_subwith[$relationship]) $row->{$relationship} = $this->{$relationship . '_model'}->with($this->_subwith[$relationship])->get($row->{$options['primary_key']}); + else $row->{$relationship} = $this->{$relationship . '_model'}->get($row->{$options['primary_key']}); } else { - $row[$relationship] = $this->{$relationship . '_model'}->get($row[$options['primary_key']]); + if (isset($this->_subwith[$relationship]) && $this->_subwith[$relationship]) $row[$relationship] = $this->{$relationship . '_model'}->with($this->_subwith[$relationship])->get($row[$options['primary_key']]); + else $row[$relationship] = $this->{$relationship . '_model'}->get($row[$options['primary_key']]); } } } @@ -502,6 +593,26 @@ public function relate($row) return $row; } + /** + * Direct ActiveRecord join + */ + public function join($table, $cond, $type = '', $escape = NULL) + { + $this->_database->join($table, $cond, $type, $escape); + + return $this; + } + + /** + * Direct ActiveRecord group by + */ + public function group_by($by, $escape = NULL) + { + $this->_database->group_by($by, $escape); + + return $this; + } + /* -------------------------------------------------------------- * UTILITY METHODS * ------------------------------------------------------------ */ @@ -551,10 +662,8 @@ function dropdown() */ public function count_by() { - if ($this->soft_delete && $this->_temporary_with_deleted !== TRUE) - { - $this->_database->where($this->soft_delete_key, (bool)$this->_temporary_only_deleted); - } + + $this->add_restrictions(); $where = func_get_args(); $this->_set_where($where); @@ -567,12 +676,10 @@ public function count_by() */ public function count_all() { - if ($this->soft_delete && $this->_temporary_with_deleted !== TRUE) - { - $this->_database->where($this->soft_delete_key, (bool)$this->_temporary_only_deleted); - } - return $this->_database->count_all($this->_table); + $this->add_restrictions(); + + return $this->_database->count_all_results($this->_table); } /** @@ -780,22 +887,31 @@ public function limit($limit, $offset = 0) */ public function trigger($event, $data = FALSE, $last = TRUE) { - if (isset($this->$event) && is_array($this->$event)) + if ($this->_temporary_without_triggers) { - foreach ($this->$event as $method) + if (strpos($event, 'after_') === 0) $this->_temporary_without_triggers = FALSE; + return $data; + } + else + { + if (isset($this->$event) && is_array($this->$event)) { - if (strpos($method, '(')) + foreach ($this->$event as $method) { - preg_match('/([a-zA-Z0-9\_\-]+)(\(([a-zA-Z0-9\_\-\., ]+)\))?/', $method, $matches); + if (strpos($method, '(')) + { + preg_match('/([a-zA-Z0-9\_\-]+)(\(([a-zA-Z0-9\_\-\., ]+)\))?/', $method, $matches); - $method = $matches[1]; - $this->callback_parameters = explode(',', $matches[3]); - } + $method = $matches[1]; + $this->callback_parameters = explode(',', $matches[3]); + } - $data = call_user_func_array(array($this, $method), array($data, $last)); + $data = call_user_func_array(array($this, $method), array($data, $last)); + } } } + return $data; } @@ -901,8 +1017,8 @@ protected function _set_where($params) { $this->_database->where($params[0]); } - else if(count($params) == 2) - { + else if(count($params) == 2) + { if (is_array($params[1])) { $this->_database->where_in($params[0], $params[1]); @@ -911,11 +1027,11 @@ protected function _set_where($params) { $this->_database->where($params[0], $params[1]); } - } - else if(count($params) == 3) - { - $this->_database->where($params[0], $params[1], $params[2]); - } + } + else if(count($params) == 3) + { + $this->_database->where($params[0], $params[1], $params[2]); + } else { if (is_array($params[1])) @@ -937,4 +1053,62 @@ protected function _return_type($multi = FALSE) $method = ($multi) ? 'result' : 'row'; return $this->_temporary_return_type == 'array' ? $method . '_array' : $method; } + + /** + * Set restriction to be appended to the query + */ + public function set_restriction($restriction) + { + $this->restriction = $restriction; + } + + /** + * Don't care about restrictions on the next call + */ + public function without_restriction() + { + $this->_temporary_without_restriction = TRUE; + return $this; + } + + /** + * Add restrictions to query + */ + protected function add_restrictions($soft_delete_check = TRUE) { + if ($this->_temporary_without_restriction !== TRUE && $this->restriction) + { + $this->_database->where($this->restriction); + } + else + { + $this->_temporary_without_restriction = FALSE; + } + if ($soft_delete_check && $this->soft_delete && $this->_temporary_with_deleted !== TRUE) + { + $this->_database->where($this->soft_delete_key, (bool)$this->_temporary_only_deleted); + } + else + { + $this->_temporary_with_deleted = $this->_temporary_only_deleted = FALSE; + } + } + + /** + * Get current primary key value + * (useful for observers which don't pass the primary key, such as after_update) + */ + public function get_primary_value() + { + return $this->primary_value; + } + + /** + * Don't issue callbacks on the next call + */ + public function without_callbacks() + { + $this->_temporary_without_triggers = TRUE; + + return $this; + } } diff --git a/tests/MY_Model_test.php b/tests/MY_Model_test.php index bcde95b..99a8a86 100644 --- a/tests/MY_Model_test.php +++ b/tests/MY_Model_test.php @@ -839,7 +839,7 @@ public function test_count_by() public function test_count_all() { $this->model->_database->expects($this->once()) - ->method('count_all') + ->method('count_all_results') ->with($this->equalTo('records')) ->will($this->returnValue(200)); $this->assertEquals($this->model->count_all(), 200);