├── LICENSE ├── README.md ├── classes ├── auth │ └── automodeler │ │ └── orm.php ├── automodeler.php ├── automodeler │ ├── core.php │ ├── orm.php │ └── orm │ │ └── core.php ├── database │ └── query │ │ └── builder │ │ └── select.php └── model │ ├── foo.php │ ├── foobar.php │ ├── ormuser.php │ ├── testrole.php │ └── testuser.php ├── config └── userguide.php ├── guide └── auto-modeler │ ├── automodeler.md │ ├── index.md │ ├── menu.md │ └── orm.md ├── tests ├── test_automodeler.php ├── test_automodeler_orm.php └── test_data │ ├── testuser_relationships.xml │ └── testusers.xml └── views └── form_errors.php /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009,2010, Jeremy Bush 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # auto-modeler: Simple and easy to use Kohana 3.0 CRUD/ORM library 2 | 3 | ## What does it do? 4 | 5 | This Kohana module allows you to easily access and model your database and it's relationships. 6 | 7 | ## How does it work? 8 | 9 | Put it in your modules folder, enable it in your kohana config, and read the docs under guide/ on how to use it. 10 | 11 | -------------------------------------------------------------------------------- /classes/auth/automodeler/orm.php: -------------------------------------------------------------------------------- 1 | get_user(); 24 | 25 | if (is_object($user) AND $user instanceof Model_User AND $user->id) 26 | { 27 | // Everything is okay so far 28 | $status = TRUE; 29 | 30 | if ( ! empty($role)) 31 | { 32 | // Multiple roles to check 33 | if (is_array($role)) 34 | { 35 | // Check each role 36 | foreach ($role as $_role) 37 | { 38 | if ( ! is_numeric($_role)) 39 | { 40 | $_role = AutoModeler_ORM::factory('role')->load(db::select()->where('name', '=', $_role))->id; 41 | } 42 | 43 | // If the user doesn't have the role 44 | if ( ! $user->has('roles', $_role)) 45 | { 46 | // Set the status false and get outta here 47 | $status = FALSE; 48 | break; 49 | } 50 | } 51 | } 52 | // Single role to check 53 | else 54 | { 55 | if ( ! is_numeric($role)) 56 | { 57 | // Load the role 58 | $role = AutoModeler_ORM::factory('role')->load(db::select()->where('name', '=', $role))->id; 59 | } 60 | 61 | // Check that the user has the given role 62 | $status = $user->has('roles', $role); 63 | } 64 | } 65 | } 66 | 67 | return $status; 68 | } 69 | 70 | /** 71 | * Logs a user in. 72 | * 73 | * @param string username 74 | * @param string password 75 | * @param boolean enable autologin 76 | * @return boolean 77 | */ 78 | protected function _login($user, $password, $remember) 79 | { 80 | if ( ! is_object($user)) 81 | { 82 | $username = $user; 83 | 84 | // Load the user 85 | $user = new Model_User($username); 86 | } 87 | 88 | // If the passwords match, perform a login 89 | if ($user->has('roles', 1) AND $user->password === $password) 90 | { 91 | if ($remember === TRUE) 92 | { 93 | // Create a new autologin token 94 | $token = ORM::factory('user_token'); 95 | 96 | // Set token data 97 | $token->user_id = $user->id; 98 | $token->expires = time() + $this->_config['lifetime']; 99 | $token->save(); 100 | 101 | // Set the autologin cookie 102 | Cookie::set('authautologin', $token->token, $this->_config['lifetime']); 103 | } 104 | 105 | // Finish the login 106 | $this->complete_login($user); 107 | 108 | return TRUE; 109 | } 110 | 111 | // Login failed 112 | return FALSE; 113 | } 114 | 115 | /** 116 | * Forces a user to be logged in, without specifying a password. 117 | * 118 | * @param mixed username string, or user ORM object 119 | * @return boolean 120 | */ 121 | public function force_login($user) 122 | { 123 | if ( ! is_object($user)) 124 | { 125 | $username = $user; 126 | 127 | // Load the user 128 | $user = ORM::factory('user'); 129 | $user->where($user->unique_key($username), '=', $username)->find(); 130 | } 131 | 132 | // Mark the session as forced, to prevent users from changing account information 133 | $this->_session->set('auth_forced', TRUE); 134 | 135 | // Run the standard completion 136 | $this->complete_login($user); 137 | } 138 | 139 | /** 140 | * Logs a user in, based on the authautologin cookie. 141 | * 142 | * @return mixed 143 | */ 144 | public function auto_login() 145 | { 146 | if ($token = Cookie::get('authautologin')) 147 | { 148 | // Load the token and user 149 | $token = ORM::factory('user_token', array('token' => $token)); 150 | 151 | if ($token->loaded() AND $token->user->loaded()) 152 | { 153 | if ($token->user_agent === sha1(Request::$user_agent)) 154 | { 155 | // Save the token to create a new unique token 156 | $token->save(); 157 | 158 | // Set the new token 159 | Cookie::set('authautologin', $token->token, $token->expires - time()); 160 | 161 | // Complete the login with the found data 162 | $this->complete_login($token->user); 163 | 164 | // Automatic login was successful 165 | return $token->user; 166 | } 167 | 168 | // Token is invalid 169 | $token->delete(); 170 | } 171 | } 172 | 173 | return FALSE; 174 | } 175 | 176 | /** 177 | * Gets the currently logged in user from the session (with auto_login check). 178 | * Returns FALSE if no user is currently logged in. 179 | * 180 | * @return mixed 181 | */ 182 | public function get_user($default = NULL) 183 | { 184 | $user = parent::get_user(); 185 | 186 | if ($user === FALSE) 187 | { 188 | // check for "remembered" login 189 | $user = $this->auto_login(); 190 | } 191 | 192 | return $user; 193 | } 194 | 195 | /** 196 | * Log a user out and remove any autologin cookies. 197 | * 198 | * @param boolean completely destroy the session 199 | * @param boolean remove all tokens for user 200 | * @return boolean 201 | */ 202 | public function logout($destroy = FALSE, $logout_all = FALSE) 203 | { 204 | // Set by force_login() 205 | $this->_session->delete('auth_forced'); 206 | 207 | if ($token = Cookie::get('authautologin')) 208 | { 209 | // Delete the autologin cookie to prevent re-login 210 | Cookie::delete('authautologin'); 211 | 212 | // Clear the autologin token from the database 213 | $token = ORM::factory('user_token', array('token' => $token)); 214 | 215 | if ($token->loaded() AND $logout_all) 216 | { 217 | ORM::factory('user_token')->where('user_id', '=', $token->user_id)->delete_all(); 218 | } 219 | elseif ($token->loaded()) 220 | { 221 | $token->delete(); 222 | } 223 | } 224 | 225 | return parent::logout($destroy); 226 | } 227 | 228 | /** 229 | * Get the stored password for a username. 230 | * 231 | * @param mixed username string, or user ORM object 232 | * @return string 233 | */ 234 | public function password($user) 235 | { 236 | if ( ! is_object($user)) 237 | { 238 | $username = $user; 239 | 240 | // Load the user 241 | $user = new Model_User($username); 242 | } 243 | 244 | return $user->password; 245 | } 246 | 247 | /** 248 | * Complete the login for a user by incrementing the logins and setting 249 | * session data: user_id, username, roles. 250 | * 251 | * @param object user ORM object 252 | * @return void 253 | */ 254 | protected function complete_login($user) 255 | { 256 | $user->complete_login(); 257 | 258 | return parent::complete_login($user); 259 | } 260 | 261 | /** 262 | * Compare password with original (hashed). Works for current (logged in) user 263 | * 264 | * @param string $password 265 | * @return boolean 266 | */ 267 | public function check_password($password) 268 | { 269 | $user = $this->get_user(); 270 | 271 | if ($user === FALSE) 272 | { 273 | // nothing to compare 274 | return FALSE; 275 | } 276 | 277 | $hash = $this->hash($password); 278 | 279 | return $hash == $user->password; 280 | } 281 | 282 | } // End Auth ORM 283 | -------------------------------------------------------------------------------- /classes/automodeler.php: -------------------------------------------------------------------------------- 1 | 'rules' array 21 | protected $_rules = array(); 22 | protected $_callbacks = array(); 23 | 24 | protected $_validation = array(); 25 | 26 | protected $_validated = FALSE; 27 | 28 | protected $_lang = 'form_errors'; 29 | 30 | protected $_state = AutoModeler::STATE_NEW; 31 | 32 | const STATE_NEW = 'new'; 33 | const STATE_LOADING = 'loading'; 34 | const STATE_LOADED = 'loaded'; 35 | const STATE_DELETED = 'deleted'; 36 | 37 | // Lists available states for this model 38 | protected $_states = array( 39 | AutoModeler::STATE_NEW, 40 | AutoModeler::STATE_LOADING, 41 | AutoModeler::STATE_LOADED, 42 | AutoModeler::STATE_DELETED 43 | ); 44 | 45 | /** 46 | * The constructor enables you to either create an empty object when no 47 | * parameter is passed as $id, or it fetches a row when the parameter 48 | * is passed. 49 | * 50 | * $blog_entry = new Blog_Model(5); 51 | * 52 | * @param string $id an id to search for 53 | * 54 | */ 55 | public function __construct($id = NULL) 56 | { 57 | parent::__construct($this->_db); 58 | 59 | if ($id !== NULL) 60 | { 61 | $this->load(db::select_array($this->fields())->where($this->_table_name.'.id', '=', $id)); 62 | } 63 | elseif ($this->id) // We loaded this via mysql_result_object 64 | { 65 | $this->_state = AutoModeler::STATE_LOADED; 66 | } 67 | } 68 | 69 | /** 70 | * Loads a database result. Can be used to load a single item into this model 71 | * or return a result set of many models. You can pass any query builder object 72 | * into the first parameter to load the specific data you need. Common usage: 73 | * 74 | * $user = new Model_User; 75 | * // Load a specific row 76 | * $user->load(db::select_array($user->fields())->where('id', '=', '1')); 77 | * 78 | * // Load many rows with where 79 | * $result = Model::factory('user')->load(db::select_array($user->fields())->where('id', '>', '3'), NULL); 80 | * 81 | * // Load all rows 82 | * $result = Model::factory('user')->load(NULL, NULL); 83 | * 84 | * // Load first two rows 85 | * $result = Model::factory('user')->load(NULL, 2); 86 | * 87 | * @param Database_Query_Builder_Select $query an optional query builder object to load with 88 | * @param integer $limit a number greater than one will return a data set 89 | * 90 | * @return $this when loading one object 91 | * @return Database_Result when loading multiple results 92 | */ 93 | public function load(Database_Query_Builder_Select $query = NULL, $limit = 1) 94 | { 95 | // Start 96 | $this->_state = AutoModeler::STATE_LOADING; 97 | 98 | // Use a normal select query by default 99 | if ($query == NULL) 100 | { 101 | $query = db::select_array(array_keys($this->_data)); 102 | } 103 | 104 | // Add limit if passed 105 | if ($limit) 106 | { 107 | $query->limit($limit); 108 | } 109 | 110 | $query->from($this->_table_name); 111 | 112 | // If we are going to return a data set, we want objects back 113 | if ($limit != 1) 114 | { 115 | $query->as_object(get_class($this)); 116 | } 117 | 118 | $data = $query->execute($this->_db); 119 | 120 | if ($limit != 1) 121 | { 122 | return $data; 123 | } 124 | 125 | // Process the results with this model's logic 126 | if (count($data) AND $data = $data->current()) 127 | { 128 | $this->process_load($data); 129 | } 130 | 131 | // We are done! 132 | $this->process_load_state(); 133 | 134 | return $this; 135 | } 136 | 137 | /** 138 | * Processes a load() from a result 139 | * 140 | * @return null 141 | */ 142 | protected function process_load($data) 143 | { 144 | $this->_data = $data; 145 | } 146 | 147 | /** 148 | * Processes the object state before a load() finishes 149 | * 150 | * @return null 151 | */ 152 | public function process_load_state() 153 | { 154 | if ($this->id) 155 | { 156 | $this->_state = AutoModeler::STATE_LOADED; 157 | } 158 | else 159 | { 160 | $this->_state = AutoModeler::STATE_NEW; 161 | } 162 | } 163 | 164 | /** 165 | * Retrieve items from the $data array. 166 | * 167 | *

title?>

168 | *

content?>

169 | * 170 | * @param string $key the field name to look for 171 | * 172 | * @throws AutoModeler_Exception 173 | * 174 | * @return String 175 | */ 176 | public function __get($key) 177 | { 178 | if (array_key_exists($key, $this->_data)) 179 | return $this->_data[$key]; 180 | 181 | throw new AutoModeler_Exception('Field '.$key.' does not exist in '.get_class($this).'!', array(), ''); 182 | } 183 | 184 | /** 185 | * Set the items in the $data array. 186 | * 187 | * $blog_entry = new Model_Blog; 188 | * $blog_entry->title = 'Demo'; 189 | * $blog_entry->content = 'My awesome content'; 190 | * 191 | * @param string $key the field name to set 192 | * @param string $value the value to set to 193 | * 194 | * @throws AutoModeler_Exception 195 | * 196 | */ 197 | public function __set($key, $value) 198 | { 199 | if (array_key_exists($key, $this->_data)) 200 | { 201 | $this->_data[$key] = $value; 202 | $this->_validated = FALSE; 203 | return; 204 | } 205 | 206 | Log::instance()->add(Log::ERROR, 'Field '.$key.' does not exist in '.get_class($this).'!'); 207 | } 208 | 209 | /** 210 | * sleep method for serialization 211 | * 212 | * @return array 213 | */ 214 | public function __sleep() 215 | { 216 | // Store only information about the object without db property 217 | return array_diff(array_keys(get_object_vars($this)), array('_db')); 218 | } 219 | 220 | /** 221 | * Magic isset method to test _data 222 | * 223 | * @param string $name the property to test 224 | * 225 | * @return bool 226 | */ 227 | public function __isset($name) 228 | { 229 | return isset($this->_data[$name]); 230 | } 231 | 232 | /** 233 | * Gets/sets the object state 234 | * 235 | * @return string/$this when getting/setting 236 | */ 237 | public function state($state = NULL) 238 | { 239 | if ($state) 240 | { 241 | if ( ! in_array($state, $this->_states)) 242 | { 243 | throw new AutoModeler_Exception('Invalid state'); 244 | } 245 | 246 | $this->_state = $state; 247 | 248 | return $this; 249 | } 250 | 251 | return $this->_state; 252 | } 253 | 254 | /** 255 | * Determine if this model is in the loaded state 256 | * 257 | * @return bool 258 | */ 259 | public function loaded() 260 | { 261 | return AutoModeler::STATE_LOADED === $this->state(); 262 | } 263 | 264 | /** 265 | * Gets an array version of the model 266 | * 267 | * @return array 268 | */ 269 | public function as_array() 270 | { 271 | return $this->_data; 272 | } 273 | 274 | /** 275 | * Gets the table name for this object 276 | * 277 | * @return string 278 | */ 279 | public function get_table_name() 280 | { 281 | return $this->_table_name; 282 | } 283 | 284 | /** 285 | * The factory method returns a model instance of the model name provided. 286 | * You can also specify an id to create a specific object. Works similar to 287 | * ORM::factory(). Using this, you can chain methods off models that 288 | * shouldn't be instantiated. 289 | * 290 | * @param string $model the model name 291 | * @param int $id an id to pass to the constructor 292 | * 293 | * @return Object 294 | */ 295 | public static function factory($model, $id = NULL) 296 | { 297 | $model = 'Model_'.ucfirst($model); 298 | return new $model($id); 299 | } 300 | 301 | /** 302 | * Mass sets object properties. Never pass $_POST into this method directly. 303 | * Always use something like array_key_intersect() to filter the array. 304 | * 305 | * @param array $data the data to set 306 | * 307 | * @return null 308 | * 309 | */ 310 | public function set_fields(array $data) 311 | { 312 | foreach (array_intersect_key($data, $this->_data) as $key => $value) 313 | { 314 | $this->$key = $value; 315 | } 316 | } 317 | 318 | /** 319 | * Behaves the same as the validation object's errors() method. 320 | * Use it to retrieve an array of validation errors from the current object. 321 | * 322 | * @param string $lang the messages file to use 323 | * 324 | * @return array 325 | */ 326 | public function errors($lang = NULL) 327 | { 328 | return $this->_validation != NULL ? $this->_validation->errors($lang) : array(); 329 | } 330 | 331 | /** 332 | * Performs the validation on your model. You can use this before you save 333 | * to ensure the model is valid. save() will call this method internally. 334 | * 335 | * You can pass an existing validation object into this method. 336 | * 337 | * Returns either TRUE on success, or an array that contains a html version 338 | * of the errors and the raw errors array from the validation object. 339 | * 340 | * @param mixed $validation a manual validation object to combine the model 341 | * properties with 342 | * 343 | * @return TRUE on success 344 | * @return array with keys 'string' containing an html list of errors and 345 | * 'errors', the raw errors validation object 346 | */ 347 | public function is_valid($validation = NULL) 348 | { 349 | $data = $validation instanceof Validation ? $validation->copy($validation->as_array()+$this->_data) : Validation::factory($this->_data); 350 | 351 | $data->bind(':model', $this); 352 | 353 | foreach ($this->_rules as $field => $rules) 354 | { 355 | $data->rules($field, $rules); 356 | } 357 | 358 | if ($data->check(TRUE)) 359 | { 360 | $this->_validation = NULL; 361 | return $this->_validated = TRUE; 362 | } 363 | else 364 | { 365 | $this->_validation = $data; 366 | $errors = View::factory('form_errors')->set(array('errors' => $data->errors($this->_lang))); 367 | return array('string' => $errors->render(), 'errors' => $data->errors($this->_lang)); 368 | } 369 | } 370 | 371 | /** 372 | * Saves the model to your database. If $data['id'] is empty, it will do a 373 | * database INSERT and assign the inserted row id to $data['id']. 374 | * If $data['id'] is not empty, it will do a database UPDATE. 375 | * 376 | * @param mixed $validation a manual validation object to combine the model properties with 377 | * 378 | * @return int 379 | */ 380 | public function save($validation = NULL) 381 | { 382 | $status = $this->_validated ? TRUE : $this->is_valid($validation); 383 | 384 | if ($status === TRUE) 385 | { 386 | if ($this->state() == AutoModeler::STATE_LOADED) // Do an update 387 | { 388 | return count(db::update($this->_table_name)->set(array_diff_assoc($this->_data, array('id' => $this->_data['id'])))->where('id', '=', $this->_data['id'])->execute($this->_db)); 389 | } 390 | else // Do an insert 391 | { 392 | $columns = array_keys($this->_data); 393 | $id = db::insert($this->_table_name) 394 | ->columns($columns) 395 | ->values($this->_data)->execute($this->_db); 396 | 397 | $this->state(AutoModeler::STATE_LOADED); 398 | 399 | return ($this->_data['id'] = $id[0]); 400 | } 401 | } 402 | 403 | throw new AutoModeler_Exception($status['string'], array(), $status['errors']); 404 | } 405 | 406 | /** 407 | * Deletes the current object's associated database row. 408 | * The object will still contain valid data until it is destroyed. 409 | * 410 | * @return integer 411 | */ 412 | public function delete() 413 | { 414 | if (AutoModeler::STATE_LOADED) 415 | { 416 | $this->_state = AutoModeler::STATE_DELETED; 417 | 418 | return db::delete($this->_table_name)->where('id', '=', $this->_data['id'])->execute($this->_db); 419 | } 420 | 421 | throw new AutoModeler_Exception('Cannot delete a non-loaded model '.get_class($this).'!', array(), array()); 422 | } 423 | 424 | /** 425 | * Returns an associative array, where the keys of the array is set to $key 426 | * column of each row, and the value is set to the $display column. 427 | * You can optionally specify the $query parameter to pass to filter for 428 | * different data. 429 | * 430 | * @param array $key the key to use for the array 431 | * @param array $where the value to use for the display 432 | * @param array $where the where clause 433 | * 434 | * @return Database_Result 435 | */ 436 | public function select_list($key, $display, Database_Query_Builder_Select $query = NULL) 437 | { 438 | $rows = array(); 439 | 440 | $array_display = FALSE; 441 | $select_array = array($key); 442 | if (is_array($display)) 443 | { 444 | $array_display = TRUE; 445 | $select_array = array_merge($select_array, $display); 446 | } 447 | else 448 | { 449 | $select_array[] = $display; 450 | } 451 | 452 | if ($query) // Fetch selected rows 453 | { 454 | $query = $this->load($query->select_array($select_array), NULL); 455 | } 456 | else // Fetch all rows 457 | { 458 | $query = $this->load(db::select_array($select_array), NULL); 459 | } 460 | 461 | foreach ($query as $row) 462 | { 463 | if ($array_display) 464 | { 465 | $display_str = array(); 466 | foreach ($display as $text) 467 | $display_str[] = $row->$text; 468 | $rows[$row->$key] = implode(' - ', $display_str); 469 | } 470 | else 471 | { 472 | $rows[$row->$key] = $row->$display; 473 | } 474 | } 475 | 476 | return $rows; 477 | } 478 | 479 | /** 480 | * Returns an array of the columns in this object. 481 | * Useful for db::select_array(). 482 | * 483 | * @return array 484 | */ 485 | public function fields() 486 | { 487 | foreach ($this->_data as $key => $value) 488 | $fields[] = $this->_table_name.'.'.$key; 489 | 490 | return $fields; 491 | } 492 | 493 | /** 494 | * Returns if the specified field exists in the model 495 | * 496 | * @return bool 497 | */ 498 | public function field_exists($field) 499 | { 500 | return array_key_exists($field, $this->_data); 501 | } 502 | 503 | // Array Access Interface 504 | public function offsetExists($key) 505 | { 506 | return array_key_exists($key, $this->_data); 507 | } 508 | 509 | public function offsetSet($key, $value) 510 | { 511 | $this->__set($key, $value); 512 | } 513 | 514 | public function offsetGet($key) 515 | { 516 | return $this->$key; 517 | } 518 | 519 | public function offsetUnset($key) 520 | { 521 | $this->_data[$key] = NULL; 522 | } 523 | } 524 | 525 | class AutoModeler_Exception extends Kohana_Exception 526 | { 527 | public $errors; 528 | 529 | public function __construct($title, array $message = NULL, $errors = '') 530 | { 531 | parent::__construct($title, $message); 532 | $this->errors = $errors; 533 | } 534 | 535 | public function __toString() 536 | { 537 | return $this->message; 538 | } 539 | } 540 | -------------------------------------------------------------------------------- /classes/automodeler/orm.php: -------------------------------------------------------------------------------- 1 | _data[$key.'_id'])) 32 | { 33 | if (isset($this->_lazy[$key])) // See if we've lazy loaded it 34 | { 35 | $model = AutoModeler::factory($key); 36 | $model->process_load($this->_lazy[$key]); 37 | $model->process_load_state(); 38 | return $model; 39 | } 40 | 41 | // Get the row from the foreign table 42 | return AutoModeler::factory($key, $this->_data[$key.'_id']); 43 | } 44 | else if (isset($this->_data[$key])) 45 | return $this->_data[$key]; 46 | } 47 | 48 | /** 49 | * Magic set method, can set many to many relationships 50 | * 51 | * @param string $key the key to set 52 | * @param mixed $value the value to set the key to 53 | * 54 | * @return none 55 | */ 56 | public function __set($key, $value) 57 | { 58 | if (in_array($key, $this->_has_many)) 59 | { 60 | $related_table = AutoModeler::factory(inflector::singular($key))->get_table_name(); 61 | $this_key = inflector::singular($this->_table_name).'_id'; 62 | $f_key = inflector::singular($related_table).'_id'; 63 | 64 | // See if this is already in the join table 65 | if ( ! count(db::select('*')->from($this->_table_name.'_'.$related_table)->where($f_key, '=', $value)->where($this_key, '=', $this->_data['id'])->execute($this->_db))) 66 | { 67 | // Insert 68 | db::insert($this->_table_name.'_'.$related_table)->columns(array($f_key, $this_key))->values(array($value, $this->_data['id']))->execute($this->_db); 69 | } 70 | } 71 | else if (in_array($key, $this->_belongs_to)) 72 | { 73 | $related_table = AutoModeler::factory(inflector::singular($key))->get_table_name(); 74 | $this_key = inflector::singular($this->_table_name).'_id'; 75 | $f_key = inflector::singular($related_table).'_id'; 76 | // See if this is already in the join table 77 | if ( ! count(db::select('*')->from($related_table.'_'.$this->_table_name)->where($this_key, '=', $value)->where($f_key, '=', $this->_data['id'])->execute($this->_db))) 78 | { 79 | // Insert 80 | db::insert($related_table.'_'.$this->_table_name, array($f_key => $value, $this_key => $this->_data['id']))->execute($this->_db); 81 | } 82 | } 83 | elseif (strpos($key, ':')) // Process with 84 | { 85 | list($table, $field) = explode(':', $key); 86 | if ($table == $this->_table_name) 87 | { 88 | parent::__set($key, $value); 89 | } 90 | elseif ($field) 91 | { 92 | $this->_lazy[inflector::singular($table)][$field] = $value; 93 | } 94 | } 95 | else 96 | { 97 | parent::__set($key, $value); 98 | } 99 | } 100 | 101 | /** 102 | * Processes a load() from a result, overloaded for with() support 103 | * 104 | * @return null 105 | */ 106 | protected function process_load($data) 107 | { 108 | $parsed_data = array(); 109 | foreach ($data as $key => $value) 110 | { 111 | if (strpos($key, ':')) 112 | { 113 | list($table, $field) = explode(':', $key); 114 | if ($table == $this->_table_name) 115 | { 116 | $parsed_data[$field] = $value; 117 | } 118 | elseif ($field) 119 | { 120 | $this->_lazy[inflector::singular($table)][$field] = $value; 121 | } 122 | } 123 | else 124 | { 125 | $parsed_data[$key] = $value; 126 | } 127 | } 128 | $this->_data = $parsed_data; 129 | } 130 | 131 | /** 132 | * Loads a model with a different one 133 | * 134 | * @return $this 135 | */ 136 | public function with($model) 137 | { 138 | $this->_load_with = $model; 139 | return $this; 140 | } 141 | 142 | /** 143 | * Overload load() to use with() 144 | * 145 | * @return $this when loading one object 146 | * @return Database_MySQL_Result when loading multiple results 147 | */ 148 | public function load(Database_Query_Builder_Select $query = NULL, $limit = 1) 149 | { 150 | if ($query == NULL) 151 | { 152 | $query = db::select_array(array_keys($this->_data)); 153 | } 154 | 155 | if ($this->_load_with !== NULL) 156 | { 157 | if (is_array($this->_load_with)) 158 | { 159 | $model = current(array_keys($this->_load_with)); 160 | $alias = current(array_values($this->_load_with)); 161 | } 162 | else 163 | { 164 | $model = $this->_load_with; 165 | $alias = $this->_load_with; 166 | } 167 | 168 | $fields = array(); 169 | foreach ($this->fields() as $field) 170 | { 171 | $fields[] = array($field, str_replace($this->_table_name.'.', '', $field)); 172 | } 173 | foreach (AutoModeler_ORM::factory($model)->fields() as $field) 174 | { 175 | $fields[] = array($field, str_replace('.', ':', $field)); 176 | } 177 | 178 | $query->select_array($fields, TRUE); 179 | $join_model = Model::factory($model); 180 | $join_table = $join_model->get_table_name(); 181 | $query->join($join_table)->on($join_table.'.id', '=', $this->_table_name.'.'.$alias.'_id'); 182 | } 183 | 184 | return parent::load($query, $limit); 185 | } 186 | 187 | /** 188 | * Performs mass relations 189 | * 190 | * @param string $key the key to set 191 | * @param array $values an array of values to relate the model with 192 | * 193 | * @return none 194 | */ 195 | public function relate($key, array $values) 196 | { 197 | if (in_array($key, $this->_has_many)) 198 | { 199 | $related_table = AutoModeler::factory(inflector::singular($key))->get_table_name(); 200 | $this_key = inflector::singular($this->_table_name).'_id'; 201 | $f_key = inflector::singular($related_table).'_id'; 202 | foreach ($values as $value) 203 | // See if this is already in the join table 204 | if ( ! count(db::select('*')->from($this->_table_name.'_'.$related_table)->where($f_key, '=', $value)->where($this_key, '=', $this->_data['id'])->execute($this->_db))) 205 | { 206 | // Insert 207 | db::insert($this->_table_name.'_'.$related_table)->columns(array($f_key, $this_key))->values(array($value, $this->_data['id']))->execute($this->_db); 208 | } 209 | } 210 | } 211 | 212 | /** 213 | * Finds relations of a has_many relationship 214 | * 215 | * // Finds all roles belonging to a user 216 | * $user->find_related('roles'); 217 | * 218 | * @param string $key the model name to look for 219 | * @param Database_Query_Builder_Select $query A select object to filter results with 220 | * 221 | * @return Database_Result 222 | */ 223 | public function find_related($key, Database_Query_Builder_Select $query = NULL) 224 | { 225 | $model = 'Model_'.inflector::singular($key); 226 | 227 | $temp = new $model(); 228 | if ( ! $query) 229 | { 230 | $query = db::select_array($temp->fields()); 231 | } 232 | 233 | if ($temp->field_exists(inflector::singular($this->_table_name).'_id')) // Look for a one to many relationship 234 | { 235 | return $temp->load($query->where(inflector::singular($this->_table_name).'_id', '=', $this->id), NULL); 236 | } 237 | elseif (in_array($key, $this->_has_many)) // Get a many to many relationship. 238 | { 239 | $related_table = AutoModeler::factory(inflector::singular($key))->get_table_name(); 240 | $join_table = $this->_table_name.'_'.$related_table; 241 | $this_key = inflector::singular($this->_table_name).'_id'; 242 | $f_key = inflector::singular($related_table).'_id'; 243 | 244 | $columns = AutoModeler::factory(inflector::singular($key))->fields(); 245 | 246 | $query = $query->from($related_table)->join($join_table)->on($join_table.'.'.$f_key, '=', $related_table.'.id'); 247 | $query->where($join_table.'.'.$this_key, '=', $this->_data['id']); 248 | return $temp->load($query, NULL); 249 | } 250 | else 251 | { 252 | throw new AutoModeler_Exception('Relationship "'.$key.'" doesn\'t exist in '.get_class($this)); 253 | } 254 | } 255 | 256 | /** 257 | * Finds parents of a belongs_to model 258 | * 259 | * // Finds all users related to a role 260 | * $role->find_parent('users'); 261 | * 262 | * @param string $key the model name to look for 263 | * @param Database_Query_Builder_Select $query A select object to filter results with 264 | * 265 | * @return Database_Result 266 | */ 267 | public function find_parent($key, Database_Query_Builder_Select $query = NULL) 268 | { 269 | $parent = AutoModeler::factory(inflector::singular($key)); 270 | $columns = $parent->fields(); 271 | 272 | if ( ! $query) 273 | { 274 | $query = db::select_array($parent->fields()); 275 | } 276 | 277 | if ($this->field_exists($key.'_id')) // Look for a one to many relationship 278 | { 279 | return $parent->load($query->where('id', '=', $this->_data[$key.'_id']), NULL); 280 | } 281 | elseif(in_array($key, $this->_belongs_to)) // Get a many to many relationship. 282 | { 283 | $related_table = $parent->get_table_name(); 284 | $join_table = $related_table.'_'.$this->_table_name; 285 | $f_key = inflector::singular($this->_table_name).'_id'; 286 | $this_key = inflector::singular($related_table).'_id'; 287 | 288 | $columns = AutoModeler::factory(inflector::singular($key))->fields(); 289 | 290 | $query = $query->join($join_table)->on($join_table.'.'.$this_key, '=', $related_table.'.id')->from($related_table)->where($join_table.'.'.$f_key, '=', $this->_data['id']); 291 | return $parent->load($query, NULL); 292 | } 293 | else 294 | { 295 | throw new AutoModeler_Exception('Relationship "'.$key.'" doesn\'t exist in '.get_class($this)); 296 | } 297 | } 298 | 299 | /** 300 | * Tests if a many to many relationship exists 301 | * 302 | * Model must have a _has_many relationship with the other model, which is 303 | * passed as the first parameter in plural form without the Model_ prefix. 304 | * 305 | * The second parameter is the id of the related model to test the relationship of. 306 | * 307 | * $user = new Model_User(1); 308 | * $user->has('roles', Model_Role::LOGIN); 309 | * 310 | * @param string $key the model name to look for (plural) 311 | * @param string $value an id to search for 312 | * 313 | * @return bool 314 | */ 315 | public function has($key, $value) 316 | { 317 | $related_table = AutoModeler::factory(inflector::singular($key))->get_table_name(); 318 | $join_table = $this->_table_name.'_'.$related_table; 319 | $f_key = inflector::singular($related_table).'_id'; 320 | $this_key = inflector::singular($this->_table_name).'_id'; 321 | 322 | if (in_array($key, $this->_has_many)) 323 | { 324 | return (bool) db::select($related_table.'.id')-> 325 | from(AutoModeler::factory(inflector::singular($key))->get_table_name())-> 326 | where($join_table.'.'.$this_key, '=', $this->_data['id'])-> 327 | where($join_table.'.'.$f_key, '=', $value)-> 328 | join($join_table)->on($join_table.'.'.$f_key, '=', $related_table.'.id')-> 329 | execute($this->_db)->count(); 330 | } 331 | return FALSE; 332 | } 333 | 334 | /** 335 | * Removes a has_many relationship if you aren't using innoDB (shame on you!) 336 | * 337 | * Model must have a _has_many relationship with the other model, which is 338 | * passed as the first parameter in plural form without the Model_ prefix. 339 | * 340 | * The second parameter is the id of the related model to remove. 341 | * 342 | * @param string $key the model name to look for 343 | * @param string $id an id to search for 344 | * 345 | * @return integer 346 | */ 347 | public function remove($key, $id) 348 | { 349 | return db::delete($this->_table_name.'_'.AutoModeler::factory(inflector::singular($key))->get_table_name())->where($key.'_id', '=', $id)->where(inflector::singular($this->_table_name).'_id', '=', $this->_data['id'])->execute($this->_db); 350 | } 351 | 352 | /** 353 | * Removes all relationships of a model 354 | * 355 | * Model must have a _has_many or _belongs_to relationship with the other model, which is 356 | * passed as the first parameter in plural form without the Model_ prefix. 357 | * 358 | * @param string $key the model name to look for 359 | * 360 | * @return integer 361 | */ 362 | public function remove_all($key) 363 | { 364 | if (in_array($key, $this->_has_many)) 365 | { 366 | return db::delete($this->_table_name.'_'.AutoModeler::factory(inflector::singular($key))->get_table_name())->where(inflector::singular($this->_table_name).'_id', '=', $this->id)->execute($this->_db); 367 | } 368 | else if (in_array($key, $this->_belongs_to)) 369 | { 370 | return db::delete(AutoModeler::factory(inflector::singular($key))->get_table_name().'_'.$this->_table_name)->where(inflector::singular($this->_table_name).'_id', '=', $this->id)->execute($this->_db); 371 | } 372 | } 373 | 374 | /** 375 | * Removes a parent relationship of a belongs_to 376 | * 377 | * @param string $key the model name to look for in plural form, without Model_ prefix 378 | * 379 | * @return integer 380 | */ 381 | public function remove_parent($key) 382 | { 383 | return db::delete(AutoModeler::factory(inflector::singular($key))->get_table_name().'_'.$this->_table_name)->where(inflector::singular($this->_table_name).'_id', '=', $this->id)->execute($this->_db); 384 | } 385 | } -------------------------------------------------------------------------------- /classes/database/query/builder/select.php: -------------------------------------------------------------------------------- 1 | _select = array(); 25 | } 26 | 27 | $columns = func_get_args(); 28 | 29 | $this->_select = array_merge($this->_select, $columns); 30 | 31 | return $this; 32 | } 33 | 34 | /** 35 | * Choose the columns to select from, using an array. 36 | * 37 | * @param array list of column names or aliases 38 | * @return $this 39 | */ 40 | public function select_array(array $columns = NULL, $reset = FALSE) 41 | { 42 | if ($reset) 43 | { 44 | $this->_select = array(); 45 | } 46 | 47 | $this->_select = array_merge($this->_select, $columns); 48 | 49 | return $this; 50 | } 51 | } -------------------------------------------------------------------------------- /classes/model/foo.php: -------------------------------------------------------------------------------- 1 | '', 17 | 'name' => '', 18 | 'ormuser_id' => '', 19 | ); 20 | 21 | } // End Model_Foobar -------------------------------------------------------------------------------- /classes/model/foobar.php: -------------------------------------------------------------------------------- 1 | '', 17 | 'name' => '', 18 | 'ormuser_id' => '', 19 | ); 20 | 21 | } // End Model_Foobar -------------------------------------------------------------------------------- /classes/model/ormuser.php: -------------------------------------------------------------------------------- 1 | '', 15 | 'username' => '', 16 | 'password' => '', 17 | 'email' => '', 18 | 'last_login' => '', 19 | 'logins' => '', 20 | 'foo_id' => NULL); 21 | 22 | protected $_rules = array( 23 | 'username' => array( 24 | array('not_empty'), 25 | ), 26 | 'email' => array( 27 | array('email'), 28 | ), 29 | ); 30 | 31 | protected $_has_many = array('testroles'); 32 | } -------------------------------------------------------------------------------- /classes/model/testrole.php: -------------------------------------------------------------------------------- 1 | '', 15 | 'name' => ''); 16 | 17 | protected $_belongs_to = array('ormusers'); 18 | 19 | } // End Model_Role -------------------------------------------------------------------------------- /classes/model/testuser.php: -------------------------------------------------------------------------------- 1 | '', 15 | 'username' => '', 16 | 'password' => '', 17 | 'email' => '', 18 | 'last_login' => '', 19 | 'logins' => ''); 20 | 21 | protected $_rules = array( 22 | 'username' => array( 23 | array('not_empty'), 24 | ), 25 | 'email' => array( 26 | array('email'), 27 | ) 28 | ); 29 | 30 | /** 31 | * overload __set() to hash a password 32 | * 33 | * @return string 34 | */ 35 | public function __set($key, $value) 36 | { 37 | if ($key == 'password') 38 | { 39 | $this->_data[$key] = sha1($value); 40 | return; 41 | } 42 | 43 | return parent::__set($key, $value); 44 | } 45 | } -------------------------------------------------------------------------------- /config/userguide.php: -------------------------------------------------------------------------------- 1 | array( 6 | 7 | // This should be the path to this modules userguide pages, without the 'guide/'. Ex: '/guide/modulename/' would be 'modulename' 8 | 'auto-modeler' => array( 9 | 10 | // Whether this modules userguide pages should be shown 11 | 'enabled' => TRUE, 12 | 13 | // The name that should show up on the userguide index page 14 | 'name' => 'Auto Modeler', 15 | 16 | // A short description of this module, shown on the index page 17 | 'description' => 'Auto-Modeler CRUD/ORM library', 18 | 19 | // Copyright message, shown in the footer for this module 20 | 'copyright' => '© 2008–2011 Kohana Team', 21 | ) 22 | ) 23 | ); 24 | -------------------------------------------------------------------------------- /guide/auto-modeler/automodeler.md: -------------------------------------------------------------------------------- 1 | # AutoModeler 2 | 3 | Auto_Modeler is an extension to your models that enables rapid application development at a small cost. It enables you to easily create, edit, delete and retrieve database records. 4 | 5 | ## Setup 6 | 7 | In order to use AutoModeler with your models, copy the module to your MODPATH, and add it in your bootstrap.php file above the Database module. Then you need to add the following to your model you wish to use it with: 8 | 9 | '', 17 | 'bar' => '', 18 | 'baz' => '', 19 | ); 20 | } 21 | 22 | The $_data variable is an associative array containing the table column names and default values for the blogs table in this case. 23 | 24 | ## Working with models 25 | 26 | Working with models is very easy. 27 | 28 | ### Create a new model 29 | 30 | $foo = new Model_Foo; 31 | 32 | ### Assign data 33 | 34 | $foo->bar = 'foobar'; 35 | $foo->baz = 'bazbar'; 36 | 37 | Or use set_fields(): 38 | 39 | $foo->set_fields( 40 | 'bar' => 'foobar', 41 | 'baz' => 'bazbar', 42 | ); 43 | 44 | Note that if you use set_fields() from $_POST, make sure you filter the keys. You almost never want to assign something like `id` via set_fields(), for instance. 45 | 46 | ### Save the model 47 | 48 | $foo->save(); 49 | 50 | ## Obtain an existing model 51 | 52 | $foo = new Model_Foo(1); 53 | 54 | ## Update it's values 55 | 56 | $foo->bar = 'changed'; 57 | 58 | ## Save the model 59 | 60 | $foo->save(); 61 | 62 | ## ArrayAccess 63 | 64 | You can also access your model fields via the ArrayAccess interface: $user['username'] 65 | 66 | ## In-Model Validation 67 | 68 | AutoModeler supports in-model validation. You can defines your field rules in your model, and upon save(), the library will run validation for you on the entered fields. The process to do this is as follows: 69 | 70 | Create a $_rules array in your model. They key is the field name, and the value is an array of rules. 71 | 72 | $_rules = array('name' => array(array('required', 'alpha_dash', 'min_length' => array('2'))), 73 | 'address' => array(array('required'))); 74 | 75 | Now, when you save() your model, it will check the "name" and "address" fields with the rules provided. If validation fails, the library will throw an exception containing the failed error messages. You can use a try/catch block to detect failing validation: 76 | 77 | template->body = new View('admin/client/form'); 83 | $this->template->body->errors = ''; 84 | $this->template->body->client = $client; 85 | $this->template->body->title = 'Add'; 86 | 87 | if ($_POST) // Save the data 88 | { 89 | $client->set_fields(Arr::extract($_POST, array('name', 'address'))); 90 | 91 | try 92 | { 93 | $client->save(); 94 | $this->request->redirect('client/view/'.$client->short_name); 95 | } 96 | catch (Kohana_User_Exception $e) 97 | { 98 | $this->template->body->client = $client; 99 | $this->template->body->errors = $e; 100 | } 101 | } 102 | } 103 | 104 | In your view, you can simply echo the $errors variable to get a nice list of errors. You can also use $e->errors to get a raw array, for example if you want to display your errors inline with your fields. 105 | 106 | ### Passing pre-built validations 107 | 108 | For more advanced validations, such as password verifications that don't directly belong to the model, you can pass a pre-built validation object to save() and is_valid() and it will combine the validation objects. Below is an example: 109 | 110 | 'foobar', 119 | 'password' => 'testing', 120 | 'repeat_password' => 'tsting'); 121 | 122 | $validation = new Validation($_POST); 123 | $validation->rule('password', 'matches', array(':validation', 'password', 'repeat_password')); 124 | try 125 | { 126 | $user->set_fields($_POST); 127 | $user->save($validation); 128 | } 129 | catch (AutoModeler_Exception $e) 130 | { 131 | echo $e; 132 | echo Kohana::debug($e->errors); 133 | die(Kohana::debug($e)); 134 | } 135 | 136 | die(Kohana::debug($user)); 137 | } 138 | } 139 | 140 | If you run this, you will get an error about the passwords not matching. You can take the example from here to create advanced validation schemes for your models. -------------------------------------------------------------------------------- /guide/auto-modeler/index.md: -------------------------------------------------------------------------------- 1 | # AutoModeler Documentation 2 | 3 | Click the menu to view each section. -------------------------------------------------------------------------------- /guide/auto-modeler/menu.md: -------------------------------------------------------------------------------- 1 | ## [Auto-Modeler]() 2 | 3 | - [automodeler](automodeler) 4 | - [orm](orm) -------------------------------------------------------------------------------- /guide/auto-modeler/orm.md: -------------------------------------------------------------------------------- 1 | # AutoModeler_ORM Documentation 2 | 3 | The ORM extension adds the following methods and properties to AutoModeler: 4 | 5 | * $_has_many and $_belongs_to member variables to define related tables for many to many relationships 6 | * overloaded __get() method for one to many relationships (your column needs "child_id" convention [user_id]) 7 | * overloaded __set() method for one to many and many to many relationships 8 | * find_related($key) method for retrieving many to many relationships 9 | * remove($key, $id) to delete many to many relationships 10 | * overloaded delete() method to handle many to many relationships 11 | 12 | ## How to set up your database and model 13 | 14 | Follow these steps to set your code up: 15 | 16 | 1. Your model needs to extend AutoModeler_ORM: 17 | 18 | class Blog extends AutoModeler_ORM 19 | 20 | 2. For many to many relationships, your model needs a $_has_many and _belongs_to variable with an array of your related tables: 21 | 22 | protected $_has_many = array('cars', 'boats'); 23 | protected $_belongs_to = array('trains'); 24 | 25 | These models all need an "id" field in them as your primary key. 26 | 27 | 3. Now you need to set up join tables in your database for your related tables. The format is "parents_childs". So for our blog example, we would create three tables: blogs_cars, blogs_boats and trains_blogs. In this table, you need three columns, id, parent_id, child_id. So for our example, our columns would be: id, blog_id, car_id for the blogs_cars table; id, blog_id, boat_id for your blogs_boats table; id, train_id, blog_id for your trains_blogs table. 28 | 29 | This is all the setup! 30 | 31 | ## Working with one to many relationships 32 | 33 | $blog = new Model_Blog; 34 | 35 | ### Get a model to relate 36 | 37 | $foo = new Model_Foo(1); 38 | 39 | ### Relate foo to blog 40 | 41 | $blog->foo_id = $foo->id; 42 | $blog->save(); 43 | 44 | ### Read it back 45 | 46 | $foo = $blog->foo; // $foo is a Model_Foo with id = 1 47 | 48 | ## Working with many to many relationships 49 | 50 | $blog = new Model_Blog(1); // Need a loaded model 51 | 52 | ## Get a model to relate 53 | 54 | $car = new Model_Car(1); 55 | 56 | ### Relate them 57 | 58 | $blog->cars = $car->id; // Works right away, no need to save() 59 | 60 | ### Read the relationships back 61 | 62 | $cars = $blog->find_related('cars'); // $cars is a database result object containing the relationships 63 | 64 | ## With support 65 | 66 | AutoModeler_ORM comes with robust support for load()ing relationships with() the main model. This works with one to many relationships (you need a model_id column in your table). Suppose you have a Model_User with a `foobar` relationship: 67 | 68 | $user = new Model_User(); 69 | $user->with('foobar')->load(db::select()->where('users.id', '=', 1)); 70 | 71 | Now when you call $user->foobar, it will not run an additional query. You must use with() alongside of load(). 72 | 73 | If you'd like a model to always load with another model, you can manually specify the $_load_with variable in the model: 74 | 75 | protected $_load_with = 'foobar'; -------------------------------------------------------------------------------- /tests/test_automodeler.php: -------------------------------------------------------------------------------- 1 | 21 | * @copyright 2010 Jeremy Bush 22 | * @license http://www.opensource.org/licenses/isc-license.txt 23 | */ 24 | 25 | require_once 'PHPUnit/Extensions/Database/TestCase.php'; 26 | 27 | class AutoModeler_Test extends PHPUnit_Extensions_Database_TestCase 28 | { 29 | protected function getconnection() 30 | { 31 | $config = Kohana::config('database')->default; 32 | 33 | $pdo = new PDO('mysql:host='.$config['connection']['hostname'].';dbname='.$config['connection']['database'], $config['connection']['username'], $config['connection']['password']); 34 | return $this->createDefaultDBConnection($pdo, $config['connection']['database']); 35 | } 36 | 37 | protected function getdataset() 38 | { 39 | return $this->createFlatXMLDataSet(Kohana::find_file('tests', 'test_data/testusers', 'xml')); 40 | } 41 | 42 | /** 43 | * Provides test data for test_create_save() 44 | * 45 | * @return array 46 | */ 47 | public function provider_create_save() 48 | { 49 | return array( 50 | // $username, $password, $email, $last_login, $logins 51 | array('unit_test', 'unit_test', 'unit@test.com', time(), '0'), 52 | ); 53 | } 54 | 55 | /** 56 | * Tests creating and saving an object 57 | * 58 | * @test 59 | * @dataProvider provider_create_save 60 | * @covers AutoModeler::__construct 61 | * @covers AutoModeler::__set 62 | * @covers AutoModeler::save 63 | * @covers AutoModeler::is_valid 64 | * @covers AutoModeler::as_array 65 | * @covers AutoModeler::set_fields 66 | * @covers AutoModeler::state 67 | * @covers AutoModeler::load 68 | * @param string $str String to parse 69 | * @param array $expected Callback and its parameters 70 | */ 71 | public function test_create_save($username, $password, $email, $last_login, $logins) 72 | { 73 | $user = new Model_TestUser; 74 | $user->username = $username; 75 | $user->password = $password; 76 | $user->email = $email; 77 | $user->last_login = $last_login; 78 | $user->logins = $logins; 79 | $this->assertTrue($user->state() == AutoModeler::STATE_NEW); 80 | $user->save(); 81 | 82 | $this->assertSame( 83 | array( 84 | 'id' => $user->id, 85 | 'username' => $username, 86 | 'password' => sha1($password), 87 | 'email' => $email, 88 | 'last_login' => $last_login, 89 | 'logins' => $logins, 90 | ), 91 | $user->as_array() 92 | ); 93 | 94 | $this->assertFalse(empty($user->id)); 95 | $this->assertTrue($user->state() == AutoModeler::STATE_LOADED); 96 | 97 | $user = new Model_TestUser; 98 | $user->set_fields( 99 | array( 100 | 'username' => $username, 101 | 'password' => $password, 102 | 'email' => $email, 103 | 'last_login' => $last_login, 104 | 'logins' => $logins, 105 | ) 106 | ); 107 | $user->save(); 108 | $this->assertFalse(empty($user->id)); 109 | } 110 | 111 | /** 112 | * Provides test data for test_create_save() 113 | * 114 | * @return array 115 | */ 116 | public function provider_read_update_delete() 117 | { 118 | return array( 119 | // $id, $username, $new_username 120 | array(1, 'foobar', 'foobarbaz'), 121 | ); 122 | } 123 | 124 | /** 125 | * Tests reading and updating objects 126 | * 127 | * @test 128 | * @dataProvider provider_read_update_delete 129 | * @covers AutoModeler::save 130 | * @covers AutoModeler::delete 131 | * @covers AutoModeler::__construct 132 | * @covers AutoModeler::__get 133 | * 134 | * @param string $str String to parse 135 | * @param array $expected Callback and its parameters 136 | */ 137 | public function test_read_update_delete($id, $username, $new_username) 138 | { 139 | $user = new Model_TestUser($id); 140 | $this->assertSame($username, $user->username); 141 | 142 | $user->username = $new_username; 143 | $user->save(); 144 | 145 | $new_user = new Model_TestUser($id); 146 | $username = $new_user->username; 147 | $this->assertSame($new_username, $new_user->username); 148 | 149 | $deletions = $user->delete(); 150 | $this->assertSame(1, $deletions); 151 | $this->assertTrue($user->state() == AutoModeler::STATE_DELETED); 152 | } 153 | 154 | /** 155 | * Tests that load() does not use __set() 156 | * 157 | * @return null 158 | */ 159 | public function test_load_does_not_use_set() 160 | { 161 | $user = new Model_TestUser; 162 | $user->load(db::select_array($user->fields())->where('id', '=', '1')); 163 | 164 | $this->assertSame('60518c1c11dc0452be71a7118a43ab68e3451b82', $user->password); 165 | } 166 | 167 | /** 168 | * Tests that load() results in a model with state = AutoModeler::STATE_LOADED 169 | * 170 | * @return null 171 | */ 172 | public function test_load_results_in_loaded_models() 173 | { 174 | $user = new Model_TestUser; 175 | $users = $user->load(db::select_array($user->fields())->where('id', '=', '1'), NULL); 176 | 177 | foreach ($users as $user) 178 | { 179 | $this->assertSame(AutoModeler::STATE_LOADED, $user->state()); 180 | } 181 | } 182 | 183 | /** 184 | * Tests that passing a limit higher than 1 (or null) returns a result set 185 | * 186 | * @return null 187 | */ 188 | public function test_load_no_limit() 189 | { 190 | $result = Model::factory('testuser')->load(NULL, NULL); 191 | $this->assertTrue($result instanceof Database_Result); 192 | $this->assertTrue(count($result) == 3); 193 | 194 | $result = Model::factory('testuser')->load(NULL, 2); 195 | $this->assertTrue($result instanceof Database_Result); 196 | $this->assertTrue(count($result) == 2); 197 | 198 | $this->assertSame(AutoModeler::STATE_LOADED, $result->current()->state()); 199 | } 200 | 201 | /** 202 | * Tests deleting a non-saved object 203 | * 204 | * @test 205 | * @covers AutoModeler::delete 206 | */ 207 | public function test_delete_non_saved() 208 | { 209 | $user = new Model_TestUser; 210 | 211 | try 212 | { 213 | $user->delete(); 214 | } 215 | catch (AutoModeler_Exception $e) 216 | { 217 | $this->assertSame('Cannot delete a non-saved model Model_TestUser!', $e->getMessage()); 218 | } 219 | } 220 | 221 | /** 222 | * Tests reading and setting an invalid model property 223 | * 224 | * @test 225 | * @covers AutoModeler::__construct 226 | * @covers AutoModeler::__set 227 | * @covers AutoModeler::__get 228 | * @covers AutoModeler::__isset 229 | * @covers AutoModeler_Exception::__toString 230 | */ 231 | public function test_invalid_property() 232 | { 233 | $user = new Model_TestUser(1); 234 | 235 | $this->assertFalse(isset($user->foo)); 236 | 237 | try 238 | { 239 | $foo = $user->foo; 240 | } 241 | catch (AutoModeler_Exception $e) 242 | { 243 | $this->assertSame('Field foo does not exist in Model_TestUser!', $e->getMessage()); 244 | } 245 | 246 | try 247 | { 248 | $user->foo = 'bar'; 249 | } 250 | catch (AutoModeler_Exception $e) 251 | { 252 | $this->assertSame('Field foo does not exist in Model_TestUser!', $e->getMessage()); 253 | $this->assertSame('Field foo does not exist in Model_TestUser!', $e->__toString()); 254 | } 255 | } 256 | 257 | /** 258 | * Tests when a model save fails because of validation 259 | * 260 | * @test 261 | * @covers AutoModeler::save 262 | * @covers AutoModeler::is_valid 263 | * @covers AutoModeler::errors 264 | * @covers AutoModeler_Exception::__construct 265 | */ 266 | public function test_validation_fail() 267 | { 268 | $user = new Model_TestUser; 269 | 270 | $user->password = 'unit_test'; 271 | 272 | try 273 | { 274 | $user->save(); 275 | } 276 | catch (AutoModeler_Exception $e) 277 | { 278 | $this->assertSame('username must not be empty', $e->errors['username']); 279 | 280 | $errors = $user->errors(); 281 | $this->assertSame(array('not_empty', array('')), $errors['username']); 282 | } 283 | } 284 | 285 | /** 286 | * Tests the factory method 287 | * 288 | * @test 289 | * @covers AutoModeler::factory 290 | */ 291 | public function test_factory() 292 | { 293 | $user = AutoModeler::factory('testuser'); 294 | 295 | $this->assertTrue($user instanceof Model_TestUser); 296 | } 297 | 298 | /** 299 | * Tests serialization of an object 300 | * 301 | * @test 302 | * @covers AutoModeler::__sleep 303 | * @covers AutoModeler::__wakeup 304 | */ 305 | public function test_serialize() 306 | { 307 | $protected = "\x0*\x0"; 308 | $user = new Model_TestUser(1); 309 | 310 | $serialized = serialize($user); 311 | // Test the fields we expect back 312 | $this->assertSame( 313 | 'O:14:"Model_TestUser":9:{s:14:"'.$protected.'_table_name";s:9:"testusers";s:8:"'.$protected.'_data";a:6:{s:2:"id";s:1:"1";s:8:"username";s:6:"foobar";s:8:"password";s:40:"60518c1c11dc0452be71a7118a43ab68e3451b82";s:5:"email";s:11:"foo@bar.com";s:10:"last_login";s:5:"12345";s:6:"logins";s:2:"10";}s:9:"'.$protected.'_rules";a:2:{s:8:"username";a:1:{i:0;a:1:{i:0;s:9:"not_empty";}}s:5:"email";a:1:{i:0;a:1:{i:0;s:5:"email";}}}s:13:"'.$protected.'_callbacks";a:0:{}s:14:"'.$protected.'_validation";a:0:{}s:13:"'.$protected.'_validated";b:0;s:8:"'.$protected.'_lang";s:11:"form_errors";s:9:"'.$protected.'_state";s:6:"loaded";s:10:"'.$protected.'_states";a:4:{i:0;s:3:"new";i:1;s:7:"loading";i:2;s:6:"loaded";i:3;s:7:"deleted";}}', 314 | $serialized 315 | ); 316 | 317 | $unserialized = unserialize($serialized); 318 | $this->assertSame('foobar', $unserialized->username); 319 | } 320 | 321 | /** 322 | * Tests array access of this object 323 | * 324 | * @test 325 | * @covers AutoModeler::offsetExists 326 | * @covers AutoModeler::offsetSet 327 | * @covers AutoModeler::offsetGet 328 | * @covers AutoModeler::offsetUnset 329 | */ 330 | public function test_array_access() 331 | { 332 | $user = new Model_TestUser(1); 333 | 334 | $this->assertSame('foobar', $user['username']); 335 | $this->assertTrue(isset($user['username'])); 336 | $this->assertFalse(isset($user['foobar'])); 337 | 338 | $user['username'] = 'unit_test'; 339 | $this->assertSame('unit_test', $user['username']); 340 | 341 | unset($user['username']); 342 | $this->assertSame(NULL, $user['username']); 343 | } 344 | 345 | /** 346 | * Provides test data for test_select_list() 347 | * 348 | * @return array 349 | */ 350 | public function provider_select_list() 351 | { 352 | return array( 353 | array('id', 'username', db::select(), array('1' => 'foobar', '2' => 'foobar', '3' => 'foobar')), 354 | array('id', array('username', 'last_login'), db::select()->where('username', '=', 'foobar'), array('1' => 'foobar - 12345', '2' => 'foobar - 12345', '3' => 'foobar - 12345')), 355 | ); 356 | } 357 | 358 | /** 359 | * Tests generating an array output for an html select list 360 | * 361 | * @test 362 | * @dataProvider provider_select_list 363 | * @covers AutoModeler::select_list 364 | */ 365 | public function test_select_list($key, $display, $query, $expected) 366 | { 367 | $this->assertSame($expected, AutoModeler::factory('testuser')->select_list($key, $display, $query)); 368 | } 369 | 370 | /** 371 | * Tests that assignment to the model properties works 372 | * 373 | * @test 374 | * @return null 375 | */ 376 | public function test_assignment() 377 | { 378 | $model = new Model_TestUser; 379 | $model->username = 'foobar'; 380 | 381 | $this->assertSame($model->username, 'foobar'); 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /tests/test_automodeler_orm.php: -------------------------------------------------------------------------------- 1 | 39 | * @copyright 2010 Jeremy Bush 40 | * @license http://www.opensource.org/licenses/isc-license.txt 41 | */ 42 | 43 | require_once 'PHPUnit/Extensions/Database/TestCase.php'; 44 | 45 | class AutoModeler_ORM_Test extends PHPUnit_Extensions_Database_TestCase 46 | { 47 | protected function getconnection() 48 | { 49 | $config = Kohana::config('database')->default; 50 | 51 | $pdo = new PDO('mysql:host='.$config['connection']['hostname'].';dbname='.$config['connection']['database'], $config['connection']['username'], $config['connection']['password']); 52 | return $this->createDefaultDBConnection($pdo, $config['connection']['database']); 53 | } 54 | 55 | protected function getDataSet() 56 | { 57 | return $this->createFlatXMLDataSet(Kohana::find_file('tests', 'test_data/testuser_relationships', 'xml')); 58 | } 59 | 60 | /** 61 | * Get the currently logged set of queries from the database profiling. 62 | * 63 | * @param string $database The database the queries will be logged under. 64 | * @return array Map of queries from the Profiler class 65 | * @author Marcus Cobden 66 | */ 67 | public function getQueries($database = 'default') 68 | { 69 | $database = "database ($database)"; 70 | 71 | $groups = Profiler::groups(); 72 | if (! array_key_exists($database, $groups)) 73 | return array(); 74 | 75 | return $groups[$database]; 76 | } 77 | 78 | /** 79 | * Find the difference between two different query profiles 80 | * 81 | * @param array $before The queries before 82 | * @param array $after The queries after 83 | * @return array(int, array) Total number of new queries and a map of query => increase. 84 | * @author Marcus Cobden 85 | */ 86 | public function queryDiff(array $before, array $after) 87 | { 88 | $added = 0; 89 | $diff = array(); 90 | 91 | foreach ($after as $query => $ids) { 92 | if (! array_key_exists($query, $before)) 93 | { 94 | $cmp = count($ids); 95 | $added += $cmp; 96 | $diff[$query] = $cmp; 97 | } 98 | else 99 | { 100 | $cmp = count($ids) - count($before[$query]); 101 | if ($cmp == 0) 102 | continue; 103 | 104 | $added += $cmp; 105 | $diff[$query] = $cmp; 106 | } 107 | } 108 | 109 | return array($added, $diff); 110 | } 111 | 112 | /** 113 | * Assert that the number of queries should have increased by a certain amount. 114 | * 115 | * @param int $increase Expected increase in number of queries 116 | * @param array $before Queries before the tests 117 | * @param array $after Queries after the tests 118 | * @return void 119 | * @author Marcus Cobden 120 | */ 121 | public function assertQueryCountIncrease($increase, array $before, array $after) 122 | { 123 | list($added, $new_queries) = $this->queryDiff($before, $after); 124 | 125 | $this->assertEquals($increase, $added, "Expected to have $increase more queries, actual increase was $added."); 126 | } 127 | 128 | public function provider_find_parent() 129 | { 130 | return array( 131 | array('Model_TestRole', 1, 'ormusers', 2), 132 | array('Model_TestRole', 2, 'ormusers', 1), 133 | ); 134 | } 135 | 136 | /** 137 | * Tests __get() with a relationship 138 | * 139 | * @return null 140 | */ 141 | public function test_get() 142 | { 143 | $user = new Model_ORMUser(1); 144 | $this->assertInstanceOf('Model_Foo', $user->foo); 145 | $this->assertSame(AutoModeler::STATE_LOADED, $user->foo->state()); 146 | } 147 | 148 | /** 149 | * @dataProvider provider_find_parent 150 | * 151 | * @covers AutoModeler_ORM::find_parent 152 | */ 153 | public function test_find_parent($model_name, $model_id, $related_model, $expected_count) 154 | { 155 | $model = new $model_name($model_id); 156 | 157 | $this->assertSame($expected_count, count($model->find_parent($related_model))); 158 | } 159 | 160 | public function provider_find_parent_where() 161 | { 162 | return array( 163 | array('Model_TestRole', 1, 'ormusers', NULL, 2), 164 | array('Model_TestRole', 1, 'ormusers', db::select()->where('ormusers.username', '=', 'foobar'), 1), 165 | ); 166 | } 167 | 168 | /** 169 | * @dataProvider provider_find_parent_where 170 | * 171 | * @covers AutoModeler_ORM::find_parent 172 | */ 173 | public function test_find_parent_where($model_name, $model_id, $related_model, $where, $expected_count) 174 | { 175 | $model = new $model_name($model_id); 176 | 177 | $this->assertSame($expected_count, count($model->find_parent($related_model, $where))); 178 | } 179 | 180 | public function provider_find_related() 181 | { 182 | return array( 183 | array('Model_ORMUser', 1, 'testroles', 2), 184 | array('Model_ORMUser', 2, 'testroles', 1), 185 | array('Model_Foo', 1, 'ormusers', 3), 186 | array('Model_ORMUser', 1, 'foos', 1), 187 | ); 188 | } 189 | 190 | /** 191 | * Tests that finding relationships that don't exist throws an exception 192 | * 193 | * @dataProvider provider_find_related 194 | * @expectedException AutoModeler_Exception 195 | * 196 | * @covers AutoModeler_ORM::find_parent 197 | */ 198 | public function test_find_parent_wrong($model_name, $model_id, $related_model, $expected_count) 199 | { 200 | $model = new $model_name($model_id); 201 | 202 | $this->assertSame($expected_count, count($model->find_parent($related_model))); 203 | } 204 | 205 | /** 206 | * @dataProvider provider_find_related 207 | * 208 | * @covers AutoModeler_ORM::find_related 209 | */ 210 | public function test_find_related($model_name, $model_id, $related_model, $expected_count) 211 | { 212 | $model = new $model_name($model_id); 213 | 214 | $related = $model->find_related($related_model); 215 | 216 | $this->assertSame($expected_count, count($related)); 217 | $this->assertSame(AutoModeler::STATE_LOADED, $related->current()->state()); 218 | } 219 | 220 | public function provider_find_related_where() 221 | { 222 | return array( 223 | array('Model_ORMUser', 1, 'testroles', NULL, 2), 224 | array('Model_ORMUser', 1, 'testroles', db::select()->where('testroles.name', '=', 'Admin'), 1), 225 | ); 226 | } 227 | 228 | /** 229 | * @dataProvider provider_find_related_where 230 | * 231 | * @covers AutoModeler_ORM::find_related 232 | */ 233 | public function test_find_related_where($model_name, $model_id, $related_model, $where, $expected_count) 234 | { 235 | $model = new $model_name($model_id); 236 | 237 | $this->assertSame($expected_count, count($model->find_related($related_model, $where))); 238 | } 239 | 240 | /** 241 | * Tests that finding relationships that don't exist throws an exception 242 | * 243 | * @dataProvider provider_find_parent 244 | * @expectedException AutoModeler_Exception 245 | * 246 | * @covers AutoModeler_ORM::find_related 247 | */ 248 | public function test_find_related_wrong($model_name, $model_id, $related_model, $expected_count) 249 | { 250 | $model = new $model_name($model_id); 251 | 252 | $this->assertSame($expected_count, count($model->find_related($related_model))); 253 | } 254 | 255 | public function provider_has() 256 | { 257 | return array( 258 | array('Model_ORMUser', 1, 'testroles', 1, TRUE), 259 | array('Model_ORMUser', 1, 'testroles', 2, TRUE), 260 | array('Model_ORMUser', 2, 'testroles', 1, TRUE), 261 | array('Model_ORMUser', 2, 'testroles', 2, FALSE), 262 | array('Model_ORMUser', 3, 'testroles', 1, FALSE), 263 | array('Model_ORMUser', 3, 'testroles', 2, FALSE), 264 | 265 | array('Model_TestRole', 1, 'ormusers', 1, FALSE), 266 | array('Model_TestRole', 2, 'ormusers', 1, FALSE), 267 | ); 268 | } 269 | 270 | /** 271 | * @dataProvider provider_has 272 | * 273 | * @covers AutoModeler_ORM::has 274 | */ 275 | public function test_has($model_name, $model_id, $related_model, $related_id, $expected) 276 | { 277 | $model = new $model_name($model_id); 278 | 279 | $this->assertSame($expected, $model->has($related_model, $related_id)); 280 | } 281 | 282 | /** 283 | * Tests with() support 284 | * 285 | * @return null 286 | */ 287 | public function test_with() 288 | { 289 | $q_before = $this->getQueries(); 290 | 291 | $user = new Model_ORMUser(); 292 | $user->with('foo')->load(db::select()->where('ormusers.id', '=', 1)); 293 | 294 | // There should only be one query 295 | $this->assertQueryCountIncrease(1, $q_before, $this->getQueries()); 296 | $this->assertTrue($user instanceof Model_ORMUser); 297 | 298 | $this->assertTrue($user->foo instanceof Model_Foo); 299 | $this->assertQueryCountIncrease(1, $q_before, $this->getQueries()); 300 | 301 | // Make sure the load()ed model is loaded() 302 | $this->assertSame(AutoModeler::STATE_LOADED, $user->foo->state()); 303 | } 304 | 305 | public function provider_remove() 306 | { 307 | return array( 308 | array('Model_ORMUser', 1, 'testrole', 1, 1), 309 | array('Model_ORMUser', 1, 'testrole', 2, 1), 310 | array('Model_ORMUser', 2, 'testrole', 1, 1), 311 | array('Model_ORMUser', 2, 'testrole', 2, 0), 312 | array('Model_ORMUser', 3, 'testrole', 1, 0), 313 | array('Model_ORMUser', 3, 'testrole', 2, 0), 314 | ); 315 | } 316 | 317 | /** 318 | * @dataProvider provider_remove 319 | * 320 | * @covers AutoModeler_ORM::remove 321 | */ 322 | public function test_remove($model_name, $model_id, $related_model, $related_id, $expected) 323 | { 324 | $model = new $model_name($model_id); 325 | 326 | $this->assertSame($expected, $model->remove($related_model, $related_id)); 327 | $this->assertFalse($model->has($related_model, $related_id)); 328 | } 329 | 330 | public function provider_remove_all() 331 | { 332 | return array( 333 | array('Model_ORMUser', 1, 'testroles', 2), 334 | array('Model_ORMUser', 2, 'testroles', 1), 335 | 336 | array('Model_TestRole', 1, 'ormusers', 2), 337 | array('Model_TestRole', 2, 'ormusers', 1), 338 | 339 | // Invalid relationships 340 | array('Model_ORMUser', 1, 'ormusers', NULL), 341 | array('Model_TestRole', 1, 'testroles', NULL), 342 | ); 343 | } 344 | 345 | /** 346 | * @dataProvider provider_remove_all 347 | * 348 | * @covers AutoModeler_ORM::remove_all 349 | */ 350 | public function test_remove_all($model_name, $model_id, $related_model, $expected) 351 | { 352 | $model = new $model_name($model_id); 353 | 354 | $this->assertSame($expected, $model->remove_all($related_model)); 355 | } 356 | 357 | public function provider_remove_parent() 358 | { 359 | return array( 360 | array('Model_TestRole', 1, 'ormusers', 2), 361 | array('Model_TestRole', 2, 'ormusers', 1), 362 | ); 363 | } 364 | 365 | /** 366 | * @dataProvider provider_remove_parent 367 | * 368 | * @covers AutoModeler_ORM::remove_parent 369 | */ 370 | public function test_remove_parent($model_name, $model_id, $related_model, $expected) 371 | { 372 | $model = new $model_name($model_id); 373 | 374 | $this->assertSame($expected, $model->remove_parent($related_model)); 375 | $this->assertSame(0, count($model->find_parent($related_model))); 376 | } 377 | 378 | /** 379 | * Tests that assignment to the model properties works 380 | * 381 | * @test 382 | * @return null 383 | */ 384 | public function test_assignment() 385 | { 386 | $model = new Model_ORMUser; 387 | $model->username = 'foobar'; 388 | 389 | $this->assertSame($model->username, 'foobar'); 390 | } 391 | } -------------------------------------------------------------------------------- /tests/test_data/testuser_relationships.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/test_data/testusers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /views/form_errors.php: -------------------------------------------------------------------------------- 1 | 2 | 7 | --------------------------------------------------------------------------------