├── LICENSE ├── README.md ├── lib ├── case_conversion.php ├── hash.php ├── model.php └── validator.php ├── sample.php └── test ├── lib ├── case_conversion.php ├── terminal_color.php └── test.php ├── run_tests.php └── tests ├── .DS_Store ├── hash.php └── model.php /LICENSE: -------------------------------------------------------------------------------- 1 | The Azure License 2 | 3 | Copyright (c) 2011 Kenneth Ballenegger 4 | 5 | Attribute to Kenneth Ballenegger - http://kswizz.com/ 6 | 7 | You (the licensee) are hereby granted permission, free of charge, to deal in this software or source code (this "Software") without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sublicense this Software, subject to the following conditions: 8 | 9 | You must give attribution to the party mentioned above, by name and by hyperlink, in the about box, credits document and/or documentation of any derivative work using a substantial portion of this Software. 10 | 11 | You may not use the name of the copyright holder(s) to endorse or promote products derived from this Software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THIS SOFTWARE OR THE USE OR OTHER DEALINGS IN THIS SOFTWARE. 14 | 15 | http://license.azuretalon.com/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MongoModel 2 | 3 | MongoModel is a simple and lightweight ORM for MongoDB and PHP. 4 | 5 | Like Mongo, it is schema-less. It lets you build native PHP model objects, while automatically taking care of Mongo persistence for you. It also takes care of tricky common problems for you: *relationships*, *caching* and *validations*. 6 | 7 | # Installation & Usage 8 | 9 | 10 | ### Setup 11 | 12 | This is required for opening up the connection. Put this in your config.php or whatever. 13 | 14 | $mongo_host = 'localhost'; 15 | $mongo_port = 27017; 16 | $mongo_database = 'test'; 17 | $mongo = new Mongo($mongo_host.':'.$mongo_port); 18 | $GLOBALS['db'] = $mongo->{$mongo_database}; 19 | 20 | require_once 'model.php'; 21 | 22 | 23 | This is how you write a model. 24 | 25 | class Example extends MongoModel { 26 | 27 | // You're not required to define anything. MongoModel will detect what you need automatically. 28 | // However if you need more control, there are more advanced examples in sample.php. 29 | } 30 | 31 | 32 | ### Usage 33 | 34 | #### Creating objects 35 | 36 | // Another method 37 | $example_1 = new Example; 38 | $example_1->textfield = 'something'; 39 | $example_1->numberfield = 4567; 40 | $example_1->save(); 41 | 42 | var_dump($example_1->_id); // `_id` contains a MongoID. 43 | var_dump($example_1->id); // `id` is the string representation of the Mongo ID. 44 | 45 | #### Querying objects 46 | 47 | // Find many 48 | $examples_2 = Example::find_many(array('textfield' => 'something')); 49 | // Use any type of Mongo query here. See Mongo docs for more examples. 50 | 51 | var_dump($examples_2); // Is an array of Example objects. 52 | 53 | // Find one 54 | $example_3 = Example::find_one(array('numberfield' => 4567)); 55 | // If more than one match exist, the first one is returned. 56 | 57 | var_dump($example_3); // Is an Example object. 58 | 59 | $example_4 = Example::find_one(array('id' => $example_1->id, 'textfield' => 'something')); 60 | // If you use `id` in a query, MongoModel will automatically translate it to `_id` as a MongoID object. 61 | 62 | // Find by ID 63 | $example_5 = Example::find_by_id($example_1->id); 64 | 65 | #### Modifying objects 66 | $example_5->textfield = 'something else'; 67 | $example_5->save(); 68 | 69 | 70 | **Check out sample.php for more detailed examples.** 71 | 72 | -------------------------------------------------------------------------------- /lib/case_conversion.php: -------------------------------------------------------------------------------- 1 | $key); 15 | } 16 | 17 | protected static function _array_to_object($array) { 18 | if(!is_assoc($array)) { 19 | return $array; 20 | } 21 | 22 | $object = new Hash(); 23 | if (is_array($array) && count($array) > 0) { 24 | foreach ($array as $name=>$value) { 25 | $name = trim($name); // no strtolower, but please use lowercase, it's prettier. 26 | if (!empty($name)) { 27 | $object->$name = Hash::_array_to_object($value); 28 | } 29 | } 30 | return $object; 31 | } else if (is_array($array) && count($array) == 0) { 32 | return new Hash; 33 | } else { 34 | return null; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/model.php: -------------------------------------------------------------------------------- 1 | ids = $ids; 47 | $this->from = $from; // is an instance 48 | $this->key = $key; // the key of the relationship in $from 49 | $this->to = $to; // is a class name 50 | 51 | $this->valid = true; 52 | } else { 53 | return false; 54 | } 55 | } 56 | 57 | public function contains($object) { 58 | return in_array($object->id, $this->ids); 59 | } 60 | 61 | public function add($object) { 62 | if ($object instanceof $this->to) { 63 | $this->from->array_push($this->key, $object->id); 64 | return $this->from->save(); 65 | } else { 66 | return false; 67 | } 68 | } 69 | 70 | public function delete($object) { 71 | if ($object instanceof $this->to) { 72 | $this->from->array_unset_value($this->key, $object->id); 73 | $this->from->save(); 74 | } else { 75 | return false; 76 | } 77 | } 78 | 79 | public function count() { 80 | return count($this->ids); 81 | } 82 | 83 | // Iterator Interface 84 | 85 | public function current() { 86 | $id = current($this->ids); 87 | $class = $this->to; 88 | return $class::find_by_id($id); 89 | } 90 | 91 | // Boilerplate code 92 | 93 | public function rewind() { 94 | reset($this->ids); 95 | } 96 | 97 | public function key() { 98 | return key($this->ids); 99 | } 100 | 101 | public function next() { 102 | return next($this->ids); 103 | } 104 | 105 | public function valid() { 106 | $key = key($this->ids); 107 | return ($key !== null && $key !== false); 108 | } 109 | } 110 | 111 | class MongoModel_ManyToManyRelationship extends MongoModel_OneToManyRelationship { 112 | 113 | protected $foreign_key = null; 114 | 115 | public function __construct($ids, $from, $key, $to, $foreign_key) { 116 | $this->foreign_key = $foreign_key; 117 | return parent::__construct($ids, $from, $key, $to); 118 | } 119 | 120 | public function add($object, $non_reciprocal = false) { 121 | if (!$non_reciprocal) { 122 | $foreign_key = $this->foreign_key; 123 | $object->__get($foreign_key)->add($this->from, true); 124 | $object->save(); 125 | } 126 | return parent::add($object); 127 | } 128 | 129 | public function delete($object, $non_reciprocal = false) { 130 | if (!$non_reciprocal) { 131 | $foreign_key = $this->foreign_key; 132 | $object->__get($foreign_key)->delete($this->from, true); 133 | $object->save(); 134 | } 135 | return parent::delete($object); 136 | } 137 | } 138 | 139 | abstract class MongoModel { 140 | 141 | // static 142 | public static $_collection = null; 143 | 144 | protected static $_has_many = array(); 145 | protected static $_has_many_to_many = array(); 146 | protected static $_has_one = array(); 147 | protected static $_has_one_to_one = array(); 148 | protected static $_defined = false; 149 | 150 | // non-static 151 | protected $_data = array(); 152 | private $date_modified_override = null; 153 | public $_errors = array(); 154 | 155 | private $_relationship_object_cache = array(); 156 | 157 | /** 158 | * Replaces any reference to to `id` with a proper `_id` MongoID query in given array 159 | * 160 | * @static 161 | * @param array 162 | * @return void 163 | * @author Kenneth Ballenegger 164 | */ 165 | final protected static function _replace_mongo_id_recursively(&$array) { 166 | if (empty($array)) return; 167 | foreach ($array as $key => $value) { 168 | if (is_array($value)) { 169 | MongoModel::_replace_mongo_id_recursively($value); 170 | } else if ($key == 'id') { 171 | $array['_id'] = new MongoID($value); 172 | unset($array['id']); 173 | } 174 | } 175 | } 176 | 177 | protected static function _prepare_query($query) { 178 | MongoModel::_replace_mongo_id_recursively($query); 179 | return $query; 180 | } 181 | 182 | /** 183 | * Gets the name of the collection this model represents. 184 | * 185 | * @static 186 | * @return string 187 | * @author Kenneth Ballenegger 188 | */ 189 | protected static function _get_collection_name() { 190 | $class = get_called_class(); 191 | if (isset($class::$_collection) && $class::$_collection!=null) { 192 | $_collection = $class::$_collection; 193 | } else { 194 | $_collection = CaseConversion::camel_case_to_underscore($class).'s'; 195 | } 196 | return $_collection; 197 | } 198 | 199 | /** 200 | * Returns the collection object this model represents. 201 | * 202 | * @static 203 | * @param string $db_preference -- optionally pass a alternate db preference 204 | * @return MongoCollection 205 | * @author Kenneth Ballenegger 206 | */ 207 | protected static function _get_collection($db_preference = null) { 208 | $class = get_called_class(); 209 | $db = $class::_get_db($db_preference); 210 | $_collection = $class::_get_collection_name(); 211 | return $db->{$_collection}; 212 | } 213 | 214 | /** 215 | * Returns the database object 216 | * 217 | * @static 218 | * @param string $preference -- if set, tries to return alternate db 219 | * @return MongoDB 220 | * @author Kenneth Ballenegger 221 | */ 222 | protected static function _get_db($preference = null) { 223 | if ($preference && isset($GLOBALS['db_'.$preference])) { 224 | return $GLOBALS['db_'.$preference]; 225 | } else if (isset($GLOBALS['db'])) { 226 | return $GLOBALS['db']; 227 | } else { 228 | return null; 229 | } 230 | } 231 | 232 | protected static function has_one($key, $target) { 233 | $class = get_called_class(); 234 | if (!isset(self::$_has_one[$class])) self::$_has_one[$class] = array(); 235 | self::$_has_one[$class][$key] = $target; 236 | } 237 | protected static function has_one_to_one($key, $target, $foreign_key) { 238 | $class = get_called_class(); 239 | if (!isset(self::$_has_one_to_one[$class])) self::$_has_one_to_one[$class] = array(); 240 | self::$_has_one_to_one[$class][$key] = Hash::create(array('key' => $foreign_key, 'target' => $target)); 241 | } 242 | protected static function has_many($key, $target) { 243 | $class = get_called_class(); 244 | if (!isset(self::$_has_many[$class])) self::$_has_many[$class] = array(); 245 | self::$_has_many[$class][$key] = $target; 246 | } 247 | protected static function has_many_to_many($key, $target, $foreign_key) { 248 | $class = get_called_class(); 249 | if (!isset(self::$_has_many_to_many[$class])) self::$_has_many_to_many[$class] = array(); 250 | self::$_has_many_to_many[$class][$key] = Hash::create(array('key' => $foreign_key, 'target' => $target)); 251 | } 252 | 253 | public static function define() { 254 | // Override this funciton to declare relationships. 255 | // Always call: parent::define(); 256 | $class = get_called_class(); 257 | $class::$_defined = true; 258 | } 259 | 260 | public static function add($data = array()) { 261 | $class = get_called_class(); 262 | $doc = new $class; 263 | $doc->_init_data($data); 264 | $doc->save(); 265 | return $doc; 266 | } 267 | 268 | public static function find_one($query = array()) { 269 | $class = get_called_class(); 270 | $collection = $class::_get_collection('read'); 271 | $doc_data = $collection->findOne($class::_prepare_query($query)); 272 | if ($doc_data) { 273 | $doc = new $class; 274 | $doc->_init_data($doc_data); 275 | return $doc; 276 | } else 277 | return null; 278 | } 279 | 280 | public static function find_many($query = array(), $sort = null, $limit = 0, $skip = 0) { 281 | $class = get_called_class(); 282 | $collection = $class::_get_collection('read'); 283 | $cursor = $collection->find($class::_prepare_query($query)); 284 | if ($sort) { 285 | $cursor->sort($sort); 286 | } 287 | if ($skip) { 288 | $cursor->skip($skip); 289 | } 290 | if ($limit) { 291 | $cursor->limit($limit); 292 | } 293 | $docs = array(); 294 | while($cursor->hasNext()) { 295 | $current_doc_data = $cursor->getNext(); 296 | if ($current_doc_data) { 297 | $current_doc = new $class; 298 | $current_doc->_init_data($current_doc_data); 299 | $docs[] = $current_doc; 300 | } 301 | } 302 | return $docs; 303 | } 304 | 305 | public static function find_by_id($id) { 306 | if ($id) { 307 | $class = get_called_class(); 308 | $collection = $class::_get_collection('read'); 309 | $doc_data = $collection->findOne(array('_id' => new MongoID($id))); 310 | if ($doc_data) { 311 | $doc = new $class(); 312 | $doc->_init_data($doc_data); 313 | return $doc; 314 | } else 315 | return null; 316 | } 317 | } 318 | 319 | public static function count($query = null) { 320 | $class = get_called_class(); 321 | $collection = $class::_get_collection('read'); 322 | $count = $collection->count($class::_prepare_query($query)); 323 | return $count; 324 | } 325 | 326 | public static function map_reduce($map, $reduce, $query = null, $options = array()) { 327 | $class = get_called_class(); 328 | $db = $class::_get_db('mr'); // Try to grab MapReduce specific db. 329 | 330 | $command = array( 331 | 'mapreduce' => $class::_get_collection_name(), 332 | 'map' => $map, 333 | 'reduce' => $reduce 334 | ); 335 | if ($query) 336 | $command['query'] = $query; 337 | 338 | foreach ($options as $name => $option) 339 | $command[$name] = $option; 340 | 341 | if (!isset($command['out'])) 342 | $command['out'] = array('inline' => 1); 343 | 344 | $reduced = $db->command($command); 345 | 346 | if (isset($options['out']) && isset($options['out']['merge'])) 347 | $collection = $options['out']['merge']; 348 | else if (isset($options['out']) && isset($options['out']['replace'])) 349 | $collection = $options['out']['replace']; 350 | else if (isset($options['out']) && isset($options['out']['reduce'])) 351 | $collection = $options['out']['reduce']; 352 | 353 | if (isset($collection) && is_string($collection)) 354 | $results_collection = $collection; 355 | else if (isset($reduced['results'])) 356 | $results = $reduced['results']; 357 | else 358 | return false; 359 | 360 | if (isset($results_collection)) 361 | $results = $db->selectCollection($results_collection)->find(); 362 | 363 | $data = array(); 364 | foreach($results as $result) { 365 | $data[$result['_id']] = $result['value']; 366 | } 367 | return $data; 368 | } 369 | 370 | // non-static 371 | 372 | public function __construct() { 373 | $this->_data['date_created'] = time(); 374 | } 375 | 376 | public function override_date_modified($date_modified) { 377 | $this->date_modified_override = (int)$date_modified; 378 | } 379 | 380 | public function _init_data($data) { 381 | foreach($data as $key => $value) { 382 | $this->_data[$key] = $value; 383 | } 384 | } 385 | 386 | 387 | // Relationship stuff 388 | 389 | protected function _relationship_get_one($key) { 390 | if (!isset($this->_data[$key])) 391 | return null; 392 | 393 | $class = get_called_class(); 394 | $target = self::$_has_one[$class][$key]; 395 | $id = $this->_data[$key]; 396 | if (isset($this->_relationship_object_cache[$key])) { 397 | return $this->_relationship_object_cache[$key]; 398 | } else { 399 | $response = $target::find_by_id($id); 400 | $this->_relationship_object_cache[$key] = $response; 401 | return $response; 402 | } 403 | } 404 | 405 | protected function _relationship_get_one_to_one($key) { 406 | if (!isset($this->_data[$key])) 407 | return null; 408 | 409 | $class = get_called_class(); 410 | $info = self::$_has_one_to_one[$class][$key]; 411 | $target = $info->target; 412 | $id = $this->_data[$key]; 413 | if (isset($this->_relationship_object_cache[$key])) { 414 | return $this->_relationship_object_cache[$key]; 415 | } else { 416 | $response = $target::find_by_id($id); 417 | $this->_relationship_object_cache[$key] = $response; 418 | return $response; 419 | } 420 | } 421 | 422 | protected function _relationship_get_many($key) { 423 | $class = get_called_class(); 424 | $target = self::$_has_many[$class][$key]; 425 | $array = array(); 426 | if (isset($this->_data[$key])) 427 | $array = $this->_data[$key]; 428 | return new MongoModel_OneToManyRelationship($array, $this, $key, $target); 429 | } 430 | 431 | protected function _relationship_get_many_to_many($key) { 432 | $class = get_called_class(); 433 | $info = self::$_has_many_to_many[$class][$key]; 434 | $array = array(); 435 | if (isset($this->_data[$key])) 436 | $array = $this->_data[$key]; 437 | return new MongoModel_ManyToManyRelationship($array, $this, $key, $info->target, $info->key); 438 | } 439 | 440 | public function _relationship_set_one($key, $value) { 441 | if (is_string($value) || is_bool($value) || is_numeric($value) || is_null($value)) 442 | $this->_data[$key] = $value; 443 | else if ($value instanceof MongoModel) 444 | $this->_data[$key] = $value->__get('id'); 445 | else 446 | return false; 447 | } 448 | 449 | public function _relationship_set_one_to_one($key, $value, $non_reciprocal = false) { 450 | $class = get_called_class(); 451 | $info = self::$_has_one_to_one[$class][$key]; 452 | 453 | // unset old relationship first 454 | $old_object = $this->_relationship_get_one_to_one($key); 455 | if (!$non_reciprocal && $old_object instanceof MongoModel) { 456 | $old_object->_relationship_set_one_to_one($info->key, null, true); 457 | $old_object->save(true); // force saving in case a validation wouldn't allow for this. can't have dangling relationships 458 | } 459 | 460 | if (is_string($value) || is_bool($value) || is_numeric($value) || is_null($value)) { 461 | $this->_data[$key] = $value; 462 | } else if ($value instanceof MongoModel) { // this is where the magic happens 463 | $this->_data[$key] = $value->__get('id'); 464 | 465 | if (!$non_reciprocal) { // set the other side of the relationship 466 | $value->_relationship_set_one_to_one($info->key, $this, true); 467 | } 468 | } else { 469 | return false; 470 | } 471 | 472 | // TODO: figure out a way where this isn't necessary. ideally, keep track of deltas and objects to save and add them to a save pool. 473 | $this->save(); 474 | } 475 | 476 | 477 | // Helpers 478 | 479 | public function array_push($key, $value) { 480 | if (!isset($this->_data[$key]) || !is_array($this->_data[$key])) 481 | $this->_data[$key] = array(); 482 | array_push($this->_data[$key], $value); 483 | } 484 | 485 | public function array_unset_value($key, $value) { 486 | if (isset($this->_data[$key]) || is_array($this->_data[$key])) { 487 | $array = $this->_data[$key]; 488 | foreach ($array as $a_key => $a_value) { 489 | if ($a_value == $value) 490 | unset($array[$a_key]); 491 | } 492 | $this->_data[$key] = $array; 493 | } 494 | } 495 | 496 | /** 497 | * Returns an array of properties, as requested in the keys param. 498 | * Note: this will fetch relationships. 499 | * 500 | * @param array $keys of keys 501 | * @return array of key-value pairs 502 | * @author Kenneth Ballenegger 503 | */ 504 | public function extract_keys($keys) { 505 | if (!is_array($keys)) return array(); 506 | 507 | $array = array(); 508 | foreach ($keys as $key) { 509 | $val = $this->__get($key); 510 | if ($val) 511 | $array[$key] = $val; 512 | } 513 | return $array; 514 | } 515 | 516 | // only works one level deep 517 | public function is_set($key) { 518 | return (!empty($this->_data[$key]) ? true : false); 519 | } 520 | 521 | public function dealloc($key) { // Because I can't call it unset(). PHP core lib is a piece of shit. Dammit! 522 | unset($this->_data[$key]); 523 | } 524 | 525 | public function __destruct() { 526 | unset($this->_data); 527 | unset($this->date_modified_override); 528 | unset($this->_errors); 529 | unset($this->_relationship_object_cache); 530 | } 531 | 532 | public function __toArray() { 533 | $response = $this->_data; 534 | unset($response['_id']); 535 | $response['id'] = $this->id; 536 | return $response; 537 | } 538 | 539 | 540 | // Accessors 541 | 542 | public function __get($key) { 543 | $class = get_called_class(); 544 | 545 | if ($key == 'id' && isset($this->_data['_id'])) { 546 | return $this->_data['_id']->__toString(); 547 | } else if ($key == '_errors') { 548 | return $this->_errors; 549 | } else if ($key == 'validates') { // property -> function mapping 550 | return $class::validates(); 551 | } else if (isset(self::$_has_one[$class][$key])) { 552 | return $this->_relationship_get_one($key); 553 | } else if (isset(self::$_has_one_to_one[$class][$key])) { 554 | return $this->_relationship_get_one_to_one($key); 555 | } else if (isset(self::$_has_many[$class][$key])) { 556 | return $this->_relationship_get_many($key); 557 | } else if (isset(self::$_has_many_to_many[$class][$key])) { 558 | return $this->_relationship_get_many_to_many($key); 559 | } else if (isset($this->_data[$key])) { 560 | return $this->_data[$key]; 561 | } else 562 | return null; 563 | } 564 | public function __set($key, $value) { 565 | $class = get_called_class(); 566 | 567 | if ($key == '_errors') { 568 | return; 569 | } else if (isset(self::$_has_one[$class][$key])) { 570 | return $this->_relationship_set_one($key, $value); 571 | } else if (isset(self::$_has_one_to_one[$class][$key])) { 572 | return $this->_relationship_set_one_to_one($key, $value); 573 | } else if (isset(self::$_has_many[$class][$key])) { 574 | return false; 575 | } else if (isset(self::$_has_many_to_many[$class][$key])) { 576 | return false; 577 | } else if ($value instanceof MongoModel) { 578 | $this->_data[$key] = $value->__get('id'); 579 | } else { 580 | $this->_data[$key] = $value; 581 | } 582 | if (MONGOMODEL_SAVE_IMPLICITLY) 583 | $this->save(); 584 | } 585 | 586 | public function delete() { 587 | $class = get_called_class(); 588 | $collection = $class::_get_collection(); 589 | if (isset($this->_data['_id'])) { 590 | return $collection->remove(array('_id' => $this->_data['_id']), array('justOne')); 591 | } else { 592 | return false; 593 | } 594 | } 595 | 596 | public function save($force = false) { 597 | if ($this->validates() || $force) { 598 | $class = get_called_class(); 599 | $collection = $class::_get_collection(); 600 | if ($this->date_modified_override) 601 | $this->_data['date_modified'] = $this->date_modified_override; 602 | else 603 | $this->_data['date_modified'] = time(); 604 | $collection->save($this->_data); 605 | return true; 606 | } else { 607 | return false; 608 | } 609 | } 610 | 611 | 612 | // Caching 613 | 614 | /** 615 | * Read a key from the cache 616 | * 617 | * @param string $key 618 | * @return MongoModel 619 | * @author Kenneth Ballenegger 620 | */ 621 | protected static function _cache_get($key) { 622 | $cache = isset($GLOBALS['cache']) ? $GLOBALS['cache'] : null; 623 | if (!$cache) 624 | return false; 625 | $obj = $cache->get($key); 626 | return $obj; 627 | } 628 | 629 | /** 630 | * Write an object to the cache 631 | * 632 | * @param mixed $data 633 | * @param string $key 634 | * @return bool 635 | * @author Kenneth Ballenegger 636 | */ 637 | protected static function _cache_set($data, $key) { 638 | 639 | $cache = isset($GLOBALS['cache']) ? $GLOBALS['cache'] : null; 640 | if (!$cache) 641 | return false; 642 | return $cache->set($key, $data, 0, MONGOMODEL_CACHE_EXPIRATION); 643 | } 644 | 645 | /** 646 | * Remove a cached key 647 | * 648 | * @param string $key 649 | * @return mixed - cached value 650 | * @author Kenneth Ballenegger 651 | */ 652 | protected static function _cache_delete($key) { 653 | 654 | $cache = isset($GLOBALS['cache']) ? $GLOBALS['cache'] : null; 655 | if (!$cache) 656 | return false; 657 | return $cache->delete($key); 658 | } 659 | 660 | /** 661 | * Find one using cache (caches the query result and the data separately) 662 | * 663 | * WARNING: This cache is not automatically invalidated 664 | * Note: Uses a covered index where possible 665 | * 666 | * @param array $query 667 | * @return MongoModel - may also return null / false 668 | * @author Kenneth Ballenegger 669 | */ 670 | public static function find_one_cached($query = array()) { 671 | $class = get_called_class(); 672 | $query = $class::_prepare_query($query); 673 | ksort($query); 674 | $hash = md5(serialize($query)); 675 | $key = $class.'::find_one::'.$hash; 676 | 677 | $id = $class::_cache_get($key); 678 | if ($id == false) { 679 | $collection = $class::_get_collection('read'); 680 | $id_data = $collection->findOne($query, array('_id' => 1)); 681 | 682 | if (!isset($id_data['_id'])) 683 | $id = null; 684 | else if ($id_data['_id'] instanceof MongoID) 685 | $id = $id_data['_id']->__toString(); 686 | else 687 | $id = $id_data['_id']; 688 | 689 | if ($id) 690 | $class::_cache_set($id, $key); 691 | else 692 | return false; 693 | } 694 | $is_global_query = (isset($query['_globally'])) ? true : false; 695 | $object = $class::find_by_id_cached($id, $is_global_query); 696 | return $object; 697 | } 698 | 699 | /** 700 | * Find many using cache (caches the query result and the data separately) 701 | * 702 | * WARNING: This cache is not automatically invalidated 703 | * Note: Uses a covered index where possible 704 | * 705 | * @param array $query 706 | * @return array of MongoModel - may also return null / false 707 | * @author Kenneth Ballenegger 708 | */ 709 | public static function find_many_cached($query = array(), $sort = null, $limit = 0, $skip = 0) { 710 | 711 | $class = get_called_class(); 712 | $query = $class::_prepare_query($query); 713 | 714 | ksort($query); 715 | $hash = md5(serialize($query)); 716 | $hash .= md5(serialize($sort)); 717 | $hash .= md5(serialize($limit)); 718 | $hash .= md5(serialize($skip)); 719 | $key = $class.'::find_many::'.$hash; 720 | 721 | $ids = $class::_cache_get($key); 722 | if ($ids == false) { 723 | $collection = $class::_get_collection('read'); 724 | $ids_cursor = $collection->find($query, array('_id' => 1)); 725 | if ($sort) { 726 | $ids_cursor->sort($sort); 727 | } 728 | if ($skip) { 729 | $ids_cursor->skip($skip); 730 | } 731 | if ($limit) { 732 | $ids_cursor->limit($limit); 733 | } 734 | 735 | $ids = array(); 736 | foreach ($ids_cursor as $id_data) { 737 | if (!isset($id_data['_id'])) { 738 | // do nothing 739 | } else if ($id_data['_id'] instanceof MongoID) { 740 | $ids[] = $id_data['_id']->__toString(); 741 | } else { 742 | $ids[] = $id_data['_id']; 743 | } 744 | } 745 | 746 | if (empty($ids)) // if there are no results, still store this in memcache 747 | $ids = -1; 748 | 749 | $class::_cache_set($ids, $key); 750 | } 751 | $objects = array(); 752 | if ($ids == -1) // if -1, there are no objects, return immediately 753 | return $objects; 754 | 755 | $is_global_query = (isset($query['_globally'])) ? true : false; 756 | foreach ($ids as $id) 757 | $objects[] = $class::find_by_id_cached($id, $is_global_query); 758 | return $objects; 759 | } 760 | 761 | /** 762 | * Find by id using cache 763 | * 764 | * @param string $id 765 | * @return MongoModel - may also return null / false 766 | * @author Kenneth Ballenegger 767 | */ 768 | public static function find_by_id_cached($id, $is_global_query=false) { 769 | $class = get_called_class(); 770 | $key = $class.'::id::'.$id; 771 | 772 | $data = $class::_cache_get($key); 773 | if ($data == false) { 774 | if ($is_global_query) 775 | $object = $class::find_by_id_globally($id); 776 | else 777 | $object = $class::find_by_id($id); 778 | 779 | if ($object) { 780 | $data = $object->_data; 781 | $class::_cache_set($data, $key); 782 | } 783 | } else { 784 | $object = new $class; 785 | $object->_init_data($data); 786 | } 787 | 788 | return $object; 789 | } 790 | 791 | // turns a group of mongomodel objects into an assocative array 792 | public static function assoc($objects) { 793 | $objects_assoc = array(); 794 | foreach($objects as $object) { 795 | $objects_assoc[$object->id] = $object; 796 | } 797 | return $objects_assoc; 798 | } 799 | 800 | /** 801 | * Count query using cache 802 | * 803 | * @param array $query 804 | * @return int 805 | * @author Kenneth Ballenegger 806 | */ 807 | public static function count_cached($query = array()) { 808 | $class = get_called_class(); 809 | 810 | ksort($query); 811 | $hash = md5(serialize($query)); 812 | $key = $class.'::count::'.$hash; 813 | 814 | $count = $class::_cache_get($key); 815 | if (!$count) { 816 | $count = $class::count($query); 817 | $class::_cache_set($count, $key); 818 | } 819 | return $count; 820 | } 821 | 822 | /** 823 | * Saves the object to the cache, by id 824 | * 825 | * @return void 826 | * @author Kenneth Ballenegger 827 | */ 828 | public function cache_save() { 829 | $class = get_called_class(); 830 | $id = $this->id; 831 | $key = $class.'::id::'.$id; 832 | 833 | $class::_cache_set($this, $key); 834 | } 835 | 836 | 837 | // Validations 838 | 839 | final public function validates() { 840 | $this->_errors = array(); 841 | $this->validate(); 842 | if (!(count($this->_errors))) 843 | return true; 844 | else 845 | return false; 846 | } 847 | 848 | public function validate() { 849 | // override this to run validations 850 | // always call parent first 851 | 852 | // add any errors to $this->_errors[$key]; 853 | return; 854 | } 855 | 856 | final public function validate_presence_of($key) { 857 | if (!empty($this->_data[$key])) { 858 | return true; 859 | } else { 860 | $this->_errors[$key] = 'must be present'; 861 | return false; 862 | } 863 | } 864 | 865 | final public function validate_numericality_of($key) { 866 | if (!isset($this->_data[$key])) { 867 | $this->_errors[$key] = 'must be present'; 868 | return false; 869 | } else if (!is_numeric($this->_data[$key])) { 870 | $this->_errors[$key] = 'must be be numeric'; 871 | return false; 872 | } else { 873 | return true; 874 | } 875 | } 876 | 877 | final public function validate_uniqueness_of($key) { 878 | $class = get_called_class(); 879 | $id = ''; 880 | if (isset($this->_data['_id'])) 881 | $id = $this->_data['_id']; 882 | 883 | if (empty($this->_data[$key])) { 884 | $this->_errors[$key] = 'must be present'; 885 | return false; 886 | } else if ($class::count(array('_id' => array('$ne' => $id), $key => $this->_data[$key]))>0) { 887 | $this->_errors[$key] = 'must be be a valid '.$model; 888 | return false; 889 | } else { 890 | return true; 891 | } 892 | } 893 | 894 | final public function validate_presence_of_one($array) { 895 | foreach ($array as $key) { 896 | if (!empty($this->_data[$key])) { 897 | return true; 898 | } 899 | } 900 | 901 | // if got to here, all keys were empty 902 | $this->_errors[implode('_', $array)] = 'one of these keys must be present: '.implode(', ', $array); 903 | return false; 904 | } 905 | 906 | final public function validate_arrayness_of($key) { 907 | if (empty($this->_data[$key])) { 908 | $this->_errors[$key] = 'must be present'; 909 | return false; 910 | } else if (!is_array($this->_data[$key])) { 911 | $this->_errors[$key] = 'must be an array'; 912 | return false; 913 | } else { 914 | return true; 915 | } 916 | } 917 | 918 | final public function validate_relationship($key, $model) { 919 | if (empty($this->_data[$key])) { 920 | $this->_errors[$key] = 'must be present'; 921 | return false; 922 | } else if (!$model::find_by_id($this->_data[$key])) { 923 | $this->_errors[$key] = 'must be be a valid '.$model; 924 | return false; 925 | } else { 926 | return true; 927 | } 928 | } 929 | 930 | final public function validate_email($key) { 931 | if (empty($this->_data[$key])) { 932 | $this->_errors[$key] = 'must be present'; 933 | return false; 934 | } else if (!Validator::email($this->_data[$key])) { 935 | $this->_errors[$key] = 'must be a valid email address'; 936 | return false; 937 | } else { 938 | return true; 939 | } 940 | } 941 | 942 | final public function validate_url($key) { 943 | if (empty($this->_data[$key])) { 944 | $this->_errors[$key] = 'must be present'; 945 | return false; 946 | } else if (!preg_match('/\b(?:(?:https?|ftp):\/\/|www\.)[-a-z0-9+&@#\/%?=~_|!:,.;]*[-a-z0-9+&@#\/%=~_|]/i', $this->_data[$key])) { 947 | $this->_errors[$key] = 'must be be a valid url'; 948 | return false; 949 | } else { 950 | return true; 951 | } 952 | } 953 | } 954 | -------------------------------------------------------------------------------- /lib/validator.php: -------------------------------------------------------------------------------- 1 | 64) 27 | { 28 | // local part length exceeded 29 | $isValid = false; 30 | } 31 | else if ($domainLen < 1 || $domainLen > 255) 32 | { 33 | // domain part length exceeded 34 | $isValid = false; 35 | } 36 | else if ($local[0] == '.' || $local[$localLen-1] == '.') 37 | { 38 | // local part starts or ends with '.' 39 | $isValid = false; 40 | } 41 | else if (preg_match('/\\.\\./', $local)) 42 | { 43 | // local part has two consecutive dots 44 | $isValid = false; 45 | } 46 | else if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) 47 | { 48 | // character not valid in domain part 49 | $isValid = false; 50 | } 51 | else if (preg_match('/\\.\\./', $domain)) 52 | { 53 | // domain part has two consecutive dots 54 | $isValid = false; 55 | } 56 | else if (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', 57 | str_replace("\\\\","",$local))) 58 | { 59 | // character not valid in local part unless 60 | // local part is quoted 61 | if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\","",$local))) 62 | { 63 | $isValid = false; 64 | } 65 | } 66 | if ($isValid && !(checkdnsrr($domain,"MX") || checkdnsrr($domain,"A"))) 67 | { 68 | // domain not found in DNS 69 | $isValid = false; 70 | } 71 | } 72 | return $isValid; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /sample.php: -------------------------------------------------------------------------------- 1 | {$mongo_database}; 10 | 11 | require_once 'lib/model.php'; 12 | 13 | 14 | // This is how you write a model. 15 | 16 | class Example extends MongoModel { 17 | 18 | // In this case will MongoModel will by default use the connection 'examples,' which it extrapolates from the class name. 19 | 20 | // You can, however, choose another collection by setting $_collection 21 | public static $_collection = 'test_collection'; 22 | 23 | // Feel free to add any methods to the class. 24 | 25 | // MongoDB is schema-agnostic, so you do not need to define your schema. No migrations, woohoo! 26 | // Any requirements should be defined using validations. 27 | 28 | // Validations: implement the validate() method 29 | // Validations will be checked everytime save is called. 30 | public function validate() { 31 | parent::validate(); // Always call parent. 32 | 33 | // You can write your own validations here. 34 | // Test whatever you need, and if something goes wrong, add an error to $this->_errors[$key] 35 | 36 | if (!is_numeric($this->_data['numberfield'])) 37 | $this->_errors['numberfield'] = 'numberfield must be numeric'; 38 | 39 | // Or use one of the handy presets 40 | $this->validate_presence_of('textfield'); 41 | // Check out also, validate_relationship. 42 | } 43 | } 44 | 45 | 46 | // This is how you use the model. 47 | 48 | // Creating objects 49 | 50 | // One method 51 | $example_1 = Example::add(array( 52 | 'textfield' => 'something', 53 | 'numberfield' => 1234, 54 | 'arrayfield' => array(1, 2, 3, 4), 55 | 'hashfield' => array('one' => 1, 'two' => 2) 56 | )); // Saved to the database straight away 57 | // Another method 58 | $example_2 = new Example; // Not saved until save() is called 59 | $example_2->textfield = 'something'; 60 | $example_2->numberfield = 4567; 61 | $example_2->arrayfield = array(1, 2, 3, 4); 62 | $example_2->hashfield = array('one' => 1, 'two' => 2); 63 | $example_2->save(); 64 | 65 | var_dump($example_2->_id); // `_id` contains a MongoID. 66 | var_dump($example_2->id); // `id` is the string representation of the Mongo ID. 67 | 68 | // Querying objects 69 | 70 | // Find many 71 | $examples_3 = Example::find_many(array('textfield' => 'something')); // Use any type of Mongo query here. See Mongo docs for more examples. 72 | var_dump($examples_3); // Is an array of Example objects. 73 | 74 | // Find one 75 | $example_4 = Example::find_one(array('numberfield' => 4567)); // If more than one match exist, the first one is returned. 76 | var_dump($example_4); // Is an Example object. 77 | 78 | $example_5 = Example::find_one(array('id' => $example_2->id, 'textfield' => 'something')); // If you use `id` in a query, MongoModel will automatically translate it to `_id` as a MongoID object. 79 | 80 | // Find by ID 81 | $example_6 = Example::find_by_id($example_2->id); 82 | 83 | // Modifying objects 84 | $example_6->textfield = 'something else'; 85 | $example_6->save(); 86 | 87 | // Relationships are automatically converted to ids. 88 | $example_6->objectfield = $example_2; 89 | var_dump($example_6->objectfield); // String id. 90 | 91 | $example_7 = Example::find_by_id($example_6->objectfield); // To retrieve the object, use the finder. 92 | var_dump($example_7); 93 | 94 | -------------------------------------------------------------------------------- /test/lib/case_conversion.php: -------------------------------------------------------------------------------- 1 | test = $test; 31 | } 32 | } 33 | 34 | abstract class Tester { 35 | 36 | public static $_tests_history = array(); 37 | 38 | final public static function depends_on($test) { 39 | $class = get_called_class(); 40 | 41 | if (!isset($class::$_tests_history[$test])) { 42 | echo 'Test required by dependecy not run: `'.$test.'`'."\n"; 43 | $class::_test_single($test); 44 | echo "\n"; 45 | return $class::depends_on($test); 46 | } else if (isset($class::$_tests_history[$test]['running']) && $class::$_tests_history[$test]['running']) { 47 | TerminalColor::out('yellow', 'Test required by dependecy is already running. This should not happen: Make sure tests are not depending on each other recursively.'."\n"); 48 | } else if (isset($class::$_tests_history[$test]['success']) && $class::$_tests_history[$test]['success'] == false) { 49 | TerminalColor::out('red', 'Dependency failed: `'.$test.'`'."\n"); 50 | throw new TestDependencyError($test); 51 | } else if (isset($class::$_tests_history[$test]['success']) && $class::$_tests_history[$test]['success'] == true) { 52 | echo 'Dependency passed: `'.$test.'`'."\n"; 53 | } else { 54 | TerminalColor::out('yellow', 'Unexpected result.'."\n"); 55 | } 56 | } 57 | 58 | final public static function _test_single($test) { 59 | $class = get_called_class(); 60 | 61 | $prefix = 'test_'; 62 | $actual_test = true; 63 | 64 | // run pre / post normally 65 | if ($test == 'pre_test' || $test == 'post_test') { 66 | $prefix = ''; 67 | $actual_test = false; 68 | } 69 | 70 | 71 | if ($actual_test && isset($class::$_tests_history[$test])) { 72 | TerminalColor::out('yellow', 'Test already run: `'.$test.'`'."\n"); 73 | return false; 74 | } 75 | 76 | $class::$_tests_history[$test] = array( 77 | 'running' => true 78 | ); 79 | 80 | $success = true; // assume success 81 | 82 | // run pre 83 | if ($actual_test && method_exists($class, 'pre_test')) { 84 | $success *= self::_test_single('pre_test'); 85 | } 86 | 87 | if (method_exists($class, $prefix.$test) && $success) { 88 | 89 | if ($actual_test) echo "\n".'Testing: `'.$test.'`'."\n"; 90 | try { 91 | TerminalColor::yellow(); 92 | call_user_func($class.'::'.$prefix.$test); 93 | TerminalColor::reset(); 94 | $description = 'Test passed: `'.$test.'`'; 95 | $class::$_tests_history[$test]['description'] = $description; 96 | if ($actual_test) TerminalColor::out('green', $description."\n"); 97 | $success *= true; 98 | } catch (TestDependencyError $e) { 99 | $dependency = $e->test; 100 | $description = 'Test `'.$test.'` failed due to dependency on: `'.$dependency.'`'."\n"; 101 | $class::$_tests_history[$test]['description'] = $description; 102 | TerminalColor::out('red', $description."\n"); 103 | $success *= false; 104 | } catch (TestError $e) { 105 | $description = 'Test failed: `'.$test.'`'."\n".$e->getMessage(); 106 | $class::$_tests_history[$test]['description'] = $description; 107 | TerminalColor::out('red', $description."\n"); 108 | $success *= false; 109 | } 110 | } else if($success == false) { 111 | $description = 'Looks like pre_test failed...'; 112 | $class::$_tests_history[$test]['description'] = $description; 113 | TerminalColor::out('red', $description."\n"); 114 | $success *= false; 115 | } else { 116 | $description = 'Can\'t find test `'.$test.'`.'; 117 | $class::$_tests_history[$test]['description'] = $description; 118 | TerminalColor::out('red', $description."\n"); 119 | $success *= false; 120 | } 121 | 122 | // run post 123 | if ($actual_test && method_exists($class, 'post_test')) { 124 | $success *= self::_test_single('post_test'); 125 | } 126 | 127 | $class::$_tests_history[$test]['success'] = $success; 128 | $class::$_tests_history[$test]['running'] = false; 129 | return $success; 130 | } 131 | 132 | final public static function test($tests = 'all') { 133 | $class = get_called_class(); 134 | $class::$_tests_history = array(); 135 | 136 | $success = true; 137 | 138 | echo 'Testing `'.$class."`\n"; 139 | 140 | if ($tests == 'all') { 141 | $tests = $class::list_tests(); 142 | } 143 | 144 | if (is_string($tests)) 145 | $tests = array($tests); 146 | 147 | foreach($tests as $test) { 148 | $success *= $class::_test_single($test); 149 | } 150 | 151 | return $success; 152 | } 153 | 154 | final public static function list_tests() { 155 | $class = get_called_class(); 156 | $methods = get_class_methods($class); 157 | 158 | $tests = array(); 159 | foreach($methods as $method) { 160 | if (preg_match('/^test_([a-z0-9_]+)$/', $method, $matches)) { 161 | $tests[] = $matches[1]; 162 | } 163 | } 164 | return $tests; 165 | } 166 | 167 | final public static function run_tests($tests = array()) { 168 | 169 | $success = true; 170 | 171 | foreach ($tests as $test) { 172 | require_once TEST_PATH.'../tests/'.$test.'.php'; 173 | $class = CaseConversion::underscore_to_camel_case($test).'Tester'; 174 | $success *= $class::test(); 175 | echo "\n".'---'."\n\n"; 176 | } 177 | 178 | if ($success) { 179 | TerminalColor::out('green', 'TESTS PASSED'."\n"); 180 | } else { 181 | TerminalColor::out('red', 'TESTS FAILED'."\n"); 182 | } 183 | return $success; 184 | } 185 | 186 | final public static function run_all_tests() { 187 | $success = true; 188 | 189 | if ($handle = opendir(TEST_PATH.'../tests')) { 190 | $tests = array(); 191 | while (false !== ($file = readdir($handle))) { 192 | if (preg_match('/^([a-z_]+)\.php$/', $file, $matches)) { 193 | $tests[] = $matches[1]; 194 | } 195 | } 196 | closedir($handle); 197 | $success *= self::run_tests($tests); 198 | } else { 199 | TerminalColor::out('red', 'Couldn\'t open test directory'."\n"); 200 | } 201 | return $success; 202 | } 203 | } -------------------------------------------------------------------------------- /test/run_tests.php: -------------------------------------------------------------------------------- 1 | {$mongo_database}; 8 | 9 | require_once dirname(__FILE__).'/lib/test.php'; 10 | $GLOBALS['tests_path'] = dirname(__FILE__).'/tests/'; 11 | 12 | Tester::run_all_tests(); -------------------------------------------------------------------------------- /test/tests/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kballenegger/MongoModel/49e6904bab638fea8481ae2229d08add928c3768/test/tests/.DS_Store -------------------------------------------------------------------------------- /test/tests/hash.php: -------------------------------------------------------------------------------- 1 | 123, 14 | 'string' => 'something', 15 | 'object' => new stdClass, 16 | 'array' => array('one', 'two', 3), 17 | 'hash' => array( 18 | 'one' => 1, 19 | 'two' => 2, 20 | 'three' => 3, 21 | ) 22 | ); 23 | 24 | $array = Hash::create($data); 25 | 26 | if (!$array instanceof Hash) 27 | throw new TestError('Hash not created correctly.'); 28 | 29 | self::$array = $array; 30 | } 31 | 32 | public static function test_numeric() { 33 | self::depends_on('construct'); 34 | $array = self::$array; 35 | 36 | if (!$array instanceof Hash) 37 | throw new TestError('Hash statically stored not readable.'); 38 | 39 | if ($array->numeric != 123) 40 | throw new TestError('Numeric data failure.'."\n".var_export($array->numeric, true)); 41 | } 42 | 43 | public static function test_string() { 44 | self::depends_on('construct'); 45 | $array = self::$array; 46 | 47 | if (!$array instanceof Hash) 48 | throw new TestError('Hash statically stored not readable.'); 49 | 50 | if ($array->string != 'something') 51 | throw new TestError('String data failure.'."\n".var_export($array->string, true)); 52 | } 53 | 54 | public static function test_object() { 55 | self::depends_on('construct'); 56 | $array = self::$array; 57 | 58 | if (!$array instanceof Hash) 59 | throw new TestError('Hash statically stored not readable.'); 60 | 61 | if (!$array->object instanceof stdClass) 62 | throw new TestError('Object data failure.'."\n".var_export($array->object, true)); 63 | } 64 | 65 | public static function test_array() { 66 | self::depends_on('construct'); 67 | $array = self::$array; 68 | 69 | if (!$array instanceof Hash) 70 | throw new TestError('Hash statically stored not readable.'); 71 | 72 | if (!is_array($array->array)) 73 | throw new TestError('Array member is not array.'."\n".var_export($array->array, true)); 74 | 75 | if (!is_array($array->array)) 76 | throw new TestError('Array data failure.'); 77 | } 78 | 79 | public static function test_hash() { 80 | self::depends_on('construct'); 81 | $array = self::$array; 82 | 83 | if (!$array instanceof Hash) 84 | throw new TestError('Hash statically stored not readable.'); 85 | 86 | if (!$array->hash instanceof Hash) 87 | throw new TestError('Hash member is not hash.'."\n".var_export($array->hash, true)); 88 | 89 | if ($array->hash->one != 1) 90 | throw new TestError('Hash member content not as expected.'."\n".var_export($array->hash, true)); 91 | } 92 | } -------------------------------------------------------------------------------- /test/tests/model.php: -------------------------------------------------------------------------------- 1 | validate_presence_of('required_field'); 10 | } 11 | } 12 | 13 | class RelationshipTestModel extends MongoModel { 14 | 15 | public static function define() { 16 | self::has_one('target', 'TestModel'); 17 | self::has_one_to_one('one_to_one_target', 'RelationshipTestModel', 'one_to_one_target'); // to itself for simplicity's sake 18 | self::has_many('many_targets', 'TestModel'); 19 | self::has_many_to_many('many_to_many_targets', 'RelationshipTestModel', 'many_to_many_targets'); 20 | } 21 | 22 | public function validate() { 23 | parent::validate(); 24 | //$this->validate_relationship('target', 'TestModel'); 25 | } 26 | } 27 | RelationshipTestModel::define(); 28 | 29 | class ModelTester extends Tester { 30 | 31 | public static function test_adding() { 32 | $model = new TestModel; 33 | $model->required_field = 'whatever'; 34 | $model->save(); 35 | 36 | if (!$model->is_set('_id') && !TestModel::find_by_id($model->id)) 37 | throw new TestError('Problem adding new object.'); 38 | 39 | // clean up 40 | $model->delete(); 41 | } 42 | 43 | public static function test_validations() { 44 | self::depends_on('adding'); 45 | 46 | $model = new TestModel; 47 | $model->bs_field = 'whatever'; 48 | 49 | if ($model->validates) 50 | throw new TestError('Validation passes when it should not.'); 51 | 52 | $model->required_field = 'whatever'; 53 | if (!$model->validates) 54 | throw new TestError('Validation fails when it should not.'); 55 | 56 | // clean up 57 | // model never saved, no need to clean up 58 | } 59 | 60 | public static function test_relationship_validation() { 61 | return; 62 | self::depends_on('adding'); 63 | 64 | $model = new TestModel; 65 | $model->required_field = 'whatever'; 66 | $model->save(); 67 | 68 | $relationship_model = new RelationshipTestModel; 69 | 70 | if ($relationship_model->validates) 71 | throw new TestError('Validation passes when it should not.'); 72 | 73 | $relationship_model->target = $model; 74 | $relationship_model->save(); 75 | 76 | if ($relationship_model->target != $model->id) 77 | throw new TestError('Relationship auto-assignment failed.'); 78 | 79 | if (!$relationship_model->validates) 80 | throw new TestError('Validation fails when it should not.'); 81 | 82 | // clean up 83 | $model->delete(); 84 | $relationship_model->delete(); 85 | } 86 | 87 | public static function test_relationship_has_one() { 88 | self::depends_on('adding'); 89 | 90 | $model = new TestModel; 91 | $model->required_field = 'whatever'; 92 | $model->save(); 93 | 94 | $relationship_model = new RelationshipTestModel; 95 | 96 | $relationship_model->target = $model; 97 | if (!$relationship_model->save()) 98 | throw new TestError('Error saving RelationshipTestModel.'); 99 | 100 | $relationship_model = RelationshipTestModel::find_by_id($relationship_model->id); 101 | 102 | if (!$relationship_model->target instanceof TestModel) 103 | throw new TestError('Error retrieving relationship target.'."\n".var_export($relationship_model->target, true)); 104 | 105 | $model->delete(); 106 | $relationship_model->delete(); 107 | } 108 | 109 | public static function test_relationship_has_one_to_one() { 110 | self::depends_on('adding'); 111 | self::depends_on('relationship_has_one'); 112 | 113 | $relationship_model = new RelationshipTestModel; 114 | $relationship_model->save(); 115 | 116 | $relationship_model2 = new RelationshipTestModel; 117 | $relationship_model2->save(); 118 | 119 | $relationship_model->one_to_one_target = $relationship_model2; 120 | 121 | // relationship models implicitly saved 122 | $relationship_model->save(); 123 | 124 | $relationship_model = RelationshipTestModel::find_by_id($relationship_model->id); 125 | $relationship_model2 = RelationshipTestModel::find_by_id($relationship_model2->id); 126 | 127 | if (!$relationship_model->one_to_one_target->id == $relationship_model2->id) 128 | throw new TestError('One-to-one relationship target invalid.'); 129 | 130 | if (!$relationship_model2->one_to_one_target->id == $relationship_model->id) 131 | throw new TestError('Inverse one-to-one relationship target invalid.'); 132 | 133 | $relationship_model2->delete(); 134 | $relationship_model->delete(); 135 | } 136 | 137 | public static function test_relationship_has_many() { 138 | self::depends_on('adding'); 139 | self::depends_on('relationship_has_one'); 140 | 141 | $model1 = new TestModel; 142 | $model1->required_field = 'whatever'; 143 | $model1->save(); 144 | 145 | $model2 = new TestModel; 146 | $model2->required_field = 'whatever'; 147 | $model2->save(); 148 | 149 | $relationship_model = new RelationshipTestModel; 150 | 151 | $relationship_model->many_targets->add($model1); 152 | $relationship_model->many_targets->add($model2); 153 | // relationship model implicitly saved 154 | 155 | $relationship_model = RelationshipTestModel::find_by_id($relationship_model->id); 156 | 157 | if (!$relationship_model->many_targets->contains($model1)) 158 | throw new TestError('Relationship\'s `contains` method doesn\'t work.'); 159 | 160 | $counter = 0; 161 | foreach($relationship_model->many_targets as $model) { 162 | $counter++; 163 | if (!$model instanceof TestModel) 164 | throw new TestError('Error retrieving one-to-many relationship target.'."\n".var_export($model, true)); 165 | } 166 | 167 | if ($counter != 2) 168 | throw new TestError('One-to-many iterator did not iterate twice as expected.'."\n".'$counter = '.$counter); 169 | 170 | $relationship_model->many_targets->delete($model1); 171 | 172 | if ($relationship_model->many_targets->contains($model1)) 173 | throw new TestError('Relationship deletion doesn\'t work.'); 174 | 175 | $model1->delete(); 176 | $model2->delete(); 177 | $relationship_model->delete(); 178 | } 179 | 180 | public static function test_relationship_has_many_to_many() { 181 | self::depends_on('adding'); 182 | self::depends_on('relationship_has_one'); 183 | self::depends_on('relationship_has_many'); 184 | 185 | $relationship_model = new RelationshipTestModel; 186 | $relationship_model->save(); 187 | 188 | $relationship_model2 = new RelationshipTestModel; 189 | $relationship_model2->save(); 190 | $relationship_model3 = new RelationshipTestModel; 191 | $relationship_model3->save(); 192 | 193 | $relationship_model->many_to_many_targets->add($relationship_model2); 194 | $relationship_model->many_to_many_targets->add($relationship_model3); 195 | // relationship models implicitly saved 196 | $relationship_model->save(); 197 | 198 | $relationship_model = RelationshipTestModel::find_by_id($relationship_model->id); 199 | 200 | if (!$relationship_model->many_to_many_targets->contains($relationship_model2)) 201 | throw new TestError('Many-to-Many Relationship\'s `contains` method doesn\'t work.'); 202 | 203 | $counter = 0; 204 | foreach($relationship_model->many_to_many_targets as $model) { 205 | $counter++; 206 | if (!$model instanceof RelationshipTestModel) 207 | throw new TestError('Error retrieving many-to-many relationship target.'."\n".var_export($model, true)); 208 | } 209 | 210 | if ($counter != 2) 211 | throw new TestError('Many-to-many iterator did not iterate twice as expected.'."\n".'$counter = '.$counter); 212 | 213 | $counter = 0; 214 | foreach($relationship_model2->many_to_many_targets as $model) { // should only be one 215 | $counter++; 216 | if (!$model instanceof RelationshipTestModel && $model->id != $relationship_model->id) 217 | throw new TestError('Error retrieving many-to-many relationship target.'."\n".var_export($model, true)); 218 | } 219 | 220 | if ($counter != 1) 221 | throw new TestError('Many-to-many counter over $relationship_model2 did not iterate only once as expected.'."\n".'$counter = '.$counter); 222 | 223 | $relationship_model->many_to_many_targets->delete($relationship_model2); 224 | 225 | if ($relationship_model2->many_to_many_targets->contains($relationship_model)) 226 | throw new TestError('Reciprocal relationship deletion doesn\'t work.'); 227 | 228 | $relationship_model2->delete(); 229 | $relationship_model3->delete(); 230 | $relationship_model->delete(); 231 | } 232 | } --------------------------------------------------------------------------------