a tiny mvc framework for php using php-activerecord

Merge branch 'master' of kla/php-activerecord

gains eager loading (for real this time)

+349 -7
+36
lib/php-activerecord/lib/Model.php
··· 988 988 } 989 989 990 990 /** 991 + * Add a model to the given named ($name) relationship. 992 + * 993 + * @internal This should <strong>only</strong> be used by eager load 994 + * @param Model $model 995 + * @param $name of relationship for this table 996 + * @return void 997 + */ 998 + public function set_relationship_from_eager_load(Model $model=null, $name) 999 + { 1000 + $table = static::table(); 1001 + 1002 + if (($rel = $table->get_relationship($name))) 1003 + { 1004 + if ($rel->is_poly()) 1005 + { 1006 + // if the related model is null and it is a poly then we should have an empty array 1007 + if (is_null($model)) 1008 + return $this->__relationships[$name] = array(); 1009 + else 1010 + return $this->__relationships[$name][] = $model; 1011 + } 1012 + else 1013 + return $this->__relationships[$name] = $model; 1014 + } 1015 + 1016 + throw new RelationshipException("Relationship named $name has not been declared for class: {$table->class->getName()}"); 1017 + } 1018 + 1019 + /** 991 1020 * Reloads the attributes and relationships of this object from the database. 992 1021 * 993 1022 * @return Model ··· 1000 1029 $this->set_attributes($this->find($pk)->attributes); 1001 1030 $this->reset_dirty(); 1002 1031 1032 + return $this; 1033 + } 1034 + 1035 + public function __clone() 1036 + { 1037 + $this->__relationships = array(); 1038 + $this->reset_dirty(); 1003 1039 return $this; 1004 1040 } 1005 1041
+83
lib/php-activerecord/lib/Relationship.php
··· 97 97 } 98 98 99 99 /** 100 + * What is this relationship's cardinality? 101 + * 102 + * @return bool 103 + */ 104 + public function is_poly() 105 + { 106 + return $this->poly_relationship; 107 + } 108 + 109 + /** 110 + * Eagerly loads relationships for $models. 111 + * 112 + * This method takes an array of models, collects PK or FK (whichever is needed for relationship), then queries 113 + * the related table by PK/FK and attaches the array of returned relationships to the appropriately named relationship on 114 + * $models. 115 + * 116 + * @param Table $table 117 + * @param $models array of model objects 118 + * @param $attributes array of attributes from $models 119 + * @param $includes array of eager load directives 120 + * @param $query_keys -> key(s) to be queried for on included/related table 121 + * @param $model_values_keys -> key(s)/value(s) to be used in query from model which is including 122 + * @return void 123 + */ 124 + protected function query_and_attach_related_models_eagerly(Table $table, $models, $attributes, $includes=array(), $query_keys=array(), $model_values_keys=array()) 125 + { 126 + $values = array(); 127 + $options = array(); 128 + $query_key = $query_keys[0]; 129 + $model_values_key = $model_values_keys[0]; 130 + 131 + foreach ($attributes as $column => $value) 132 + $values[] = $value[$model_values_key]; 133 + 134 + $values = array($values); 135 + $options['conditions'] = SQLBuilder::create_conditions_from_underscored_string($table->conn,$query_key,$values); 136 + 137 + if (!empty($includes)) 138 + $options['include'] = $includes; 139 + 140 + $class = $this->class_name; 141 + 142 + $related_models = $class::find('all', $options); 143 + $used_models = array(); 144 + 145 + foreach ($models as $model) 146 + { 147 + $matches = 0; 148 + $key_to_match = $model->$model_values_key; 149 + 150 + foreach ($related_models as $related) 151 + { 152 + if ($related->$query_key == $key_to_match) 153 + { 154 + $hash = spl_object_hash($related); 155 + 156 + if (in_array($hash, $used_models)) 157 + $model->set_relationship_from_eager_load(clone($related), $this->attribute_name); 158 + else 159 + $model->set_relationship_from_eager_load($related, $this->attribute_name); 160 + 161 + $used_models[] = $hash; 162 + $matches++; 163 + } 164 + } 165 + 166 + if (0 === $matches) 167 + $model->set_relationship_from_eager_load(null, $this->attribute_name); 168 + } 169 + } 170 + 171 + /** 100 172 * Creates a new instance of specified {@link Model} with the attributes pre-loaded. 101 173 * 102 174 * @param Model $model The model which holds this association ··· 420 492 $attributes = $this->inject_foreign_key_for_new_association($model, $attributes); 421 493 return parent::create_association($model, $attributes); 422 494 } 495 + 496 + public function load_eagerly($models=array(), $attributes=array(), $includes, Table $table) 497 + { 498 + $this->set_keys($table->class->name); 499 + $this->query_and_attach_related_models_eagerly($table,$models,$attributes,$includes,$this->foreign_key, $table->pk); 500 + } 423 501 }; 424 502 425 503 /** ··· 539 617 $options['conditions'] = $conditions; 540 618 $class = $this->class_name; 541 619 return $class::first($options); 620 + } 621 + 622 + public function load_eagerly($models=array(), $attributes, $includes, Table $table) 623 + { 624 + $this->query_and_attach_related_models_eagerly($table,$models,$attributes,$includes, $this->primary_key,$this->foreign_key); 542 625 } 543 626 }; 544 627 ?>
+70 -5
lib/php-activerecord/lib/Table.php
··· 178 178 { 179 179 $sql = $this->options_to_sql($options); 180 180 $readonly = (array_key_exists('readonly',$options) && $options['readonly']) ? true : false; 181 - return $this->find_by_sql($sql->to_s(),$sql->get_where_values(), $readonly); 181 + $eager_load = array_key_exists('include',$options) ? $options['include'] : null; 182 + 183 + return $this->find_by_sql($sql->to_s(),$sql->get_where_values(), $readonly, $eager_load); 182 184 } 183 185 184 - public function find_by_sql($sql, $values=null, $readonly=false) 186 + public function find_by_sql($sql, $values=null, $readonly=false, $includes=null) 185 187 { 186 188 $this->last_sql = $sql; 187 189 188 - $list = array(); 190 + $collect_attrs_for_includes = is_null($includes) ? false : true; 191 + $list = $attrs = array(); 192 + 189 193 $sth = $this->conn->query($sql,$values); 190 194 191 195 while (($row = $sth->fetch())) ··· 195 199 if ($readonly) 196 200 $model->readonly(); 197 201 202 + if ($collect_attrs_for_includes) 203 + $attrs[] = $model->attributes(); 204 + 198 205 $list[] = $model; 199 206 } 207 + 208 + if ($collect_attrs_for_includes) 209 + $this->execute_eager_load($list, $attrs, $includes); 210 + 200 211 return $list; 201 212 } 202 213 214 + /** 215 + * Executes an eager load of a given named relationship for this table. 216 + * 217 + * @param $models array found modesl for this table 218 + * @param $attrs array of attrs from $models 219 + * @param $includes array eager load directives 220 + * @return void 221 + */ 222 + private function execute_eager_load($models=array(), $attrs=array(), $includes=array()) 223 + { 224 + if (!is_array($includes)) 225 + $includes = array($includes); 226 + 227 + foreach ($includes as $index => $name) 228 + { 229 + // nested include 230 + if (is_array($name)) 231 + { 232 + $nested_includes = count($name) > 1 ? $name : $name[0]; 233 + $name = $index; 234 + } 235 + else 236 + $nested_includes = array(); 237 + 238 + $rel = $this->get_relationship($name, true); 239 + $rel->load_eagerly($models, $attrs, $nested_includes, $this); 240 + } 241 + } 242 + 203 243 public function get_column_by_inflected_name($inflected_name) 204 244 { 205 245 foreach ($this->columns as $raw_name => $column) ··· 220 260 return $table; 221 261 } 222 262 223 - public function get_relationship($name) 263 + /** 264 + * Retrieve a relationship object for this table. Strict as true will throw an error 265 + * if the relationship name does not exist. 266 + * 267 + * @param $name string name of Relationship 268 + * @param $strict bool 269 + * @throws RelationshipException 270 + * @return Relationship or null 271 + */ 272 + public function get_relationship($name, $strict=false) 224 273 { 225 - if (isset($this->relationships[$name])) 274 + if ($this->has_relationship($name)) 226 275 return $this->relationships[$name]; 276 + 277 + if ($strict) 278 + throw new RelationshipException("Relationship named $name has not been declared for class: {$this->class->getName()}"); 279 + 280 + return null; 281 + } 282 + 283 + /** 284 + * Does a given relationship exist? 285 + * 286 + * @param $name string name of Relationship 287 + * @return bool 288 + */ 289 + public function has_relationship($name) 290 + { 291 + return array_key_exists($name, $this->relationships); 227 292 } 228 293 229 294 public function insert(&$data)
+154
lib/php-activerecord/test/RelationshipTest.php
··· 444 444 { 445 445 AuthorWithNonModelRelationship::first()->books; 446 446 } 447 + 448 + public function test_eager_loading_has_many() 449 + { 450 + $venues = Venue::find(array(2, 6), array('include' => 'events')); 451 + 452 + $this->assert_true(strpos(ActiveRecord\Table::load('Event')->last_sql, 'WHERE `venue_id` IN(?,?)') !== false); 453 + 454 + foreach ($venues[0]->events as $event) 455 + $this->assert_equals($event->venue_id, $venues[0]->id); 456 + 457 + $this->assert_equals(2, count($venues[0]->events)); 458 + } 459 + 460 + public function test_eager_loading_has_many_with_no_related_rows() 461 + { 462 + $venues = Venue::find(array(7, 8), array('include' => 'events')); 463 + 464 + foreach ($venues as $v) 465 + $this->assert_true(empty($v->events)); 466 + 467 + $this->assert_true(strpos(ActiveRecord\Table::load('Venue')->last_sql, 'WHERE `id` IN(?,?)') !== false); 468 + $this->assert_true(strpos(ActiveRecord\Table::load('Event')->last_sql, 'WHERE `venue_id` IN(?,?)') !== false); 469 + } 470 + 471 + public function test_eager_loading_has_many_array_of_includes() 472 + { 473 + Author::$has_many = array(array('books'), array('awesome_people')); 474 + $authors = Author::find(array(1,2), array('include' => array('books', 'awesome_people'))); 475 + 476 + $assocs = array('books', 'awesome_people'); 477 + 478 + foreach ($assocs as $assoc) 479 + { 480 + $this->assert_type('array', $authors[0]->$assoc); 481 + 482 + foreach ($authors[0]->$assoc as $a) 483 + $this->assert_equals($authors[0]->author_id,$a->author_id); 484 + } 485 + 486 + foreach ($assocs as $assoc) 487 + { 488 + $this->assert_type('array', $authors[1]->$assoc); 489 + $this->assert_true(empty($authors[1]->$assoc)); 490 + } 491 + 492 + $this->assert_true(strpos(ActiveRecord\Table::load('Author')->last_sql, 'WHERE `author_id` IN(?,?)') !== false); 493 + $this->assert_true(strpos(ActiveRecord\Table::load('Book')->last_sql, 'WHERE `author_id` IN(?,?)') !== false); 494 + $this->assert_true(strpos(ActiveRecord\Table::load('AwesomePerson')->last_sql, 'WHERE `author_id` IN(?,?)') !== false); 495 + } 496 + 497 + public function test_eager_loading_has_many_nested() 498 + { 499 + $venues = Venue::find(array(1,2), array('include' => array('events' => array('host')))); 500 + 501 + $this->assert_equals(2, count($venues)); 502 + 503 + foreach ($venues as $v) 504 + { 505 + $this->assert_true(count($v->events) > 0); 506 + 507 + foreach ($v->events as $e) 508 + { 509 + $this->assert_equals($e->host_id, $e->host->id); 510 + $this->assert_equals($v->id, $e->venue_id); 511 + } 512 + } 513 + 514 + $this->assert_true(strpos(ActiveRecord\Table::load('Venue')->last_sql, 'WHERE `id` IN(?,?)') !== false); 515 + $this->assert_true(strpos(ActiveRecord\Table::load('Event')->last_sql, 'WHERE `venue_id` IN(?,?)') !== false); 516 + $this->assert_true(strpos(ActiveRecord\Table::load('Host')->last_sql, 'WHERE `id` IN(?,?,?)') !== false); 517 + } 518 + 519 + public function test_eager_loading_belongs_to() 520 + { 521 + $events = Event::find(array(1,2,3,5,7), array('include' => 'venue')); 522 + 523 + foreach ($events as $event) 524 + $this->assert_equals($event->venue_id, $event->venue->id); 525 + 526 + $this->assert_true(strpos(ActiveRecord\Table::load('Venue')->last_sql, 'WHERE `id` IN(?,?,?,?,?)') !== false); 527 + } 528 + 529 + public function test_eager_loading_belongs_to_array_of_includes() 530 + { 531 + $events = Event::find(array(1,2,3,5,7), array('include' => array('venue', 'host'))); 532 + 533 + foreach ($events as $event) 534 + { 535 + $this->assert_equals($event->venue_id, $event->venue->id); 536 + $this->assert_equals($event->host_id, $event->host->id); 537 + } 538 + 539 + $this->assert_true(strpos(ActiveRecord\Table::load('Event')->last_sql, 'WHERE `id` IN(?,?,?,?,?)') !== false); 540 + $this->assert_true(strpos(ActiveRecord\Table::load('Host')->last_sql, 'WHERE `id` IN(?,?,?,?,?)') !== false); 541 + $this->assert_true(strpos(ActiveRecord\Table::load('Venue')->last_sql, 'WHERE `id` IN(?,?,?,?,?)') !== false); 542 + } 543 + 544 + public function test_eager_loading_belongs_to_nested() 545 + { 546 + Author::$has_many = array(array('awesome_people')); 547 + 548 + $books = Book::find(array(1,2), array('include' => array('author' => array('awesome_people')))); 549 + 550 + $assocs = array('author', 'awesome_people'); 551 + 552 + foreach ($books as $book) 553 + { 554 + $this->assert_equals($book->author_id,$book->author->author_id); 555 + $this->assert_equals($book->author->author_id,$book->author->awesome_people[0]->author_id); 556 + } 557 + 558 + $this->assert_true(strpos(ActiveRecord\Table::load('Book')->last_sql, 'WHERE `book_id` IN(?,?)') !== false); 559 + $this->assert_true(strpos(ActiveRecord\Table::load('Author')->last_sql, 'WHERE `author_id` IN(?,?)') !== false); 560 + $this->assert_true(strpos(ActiveRecord\Table::load('AwesomePerson')->last_sql, 'WHERE `author_id` IN(?,?)') !== false); 561 + } 562 + 563 + public function test_eager_loading_belongs_to_with_no_related_rows() 564 + { 565 + $e1 = Event::create(array('venue_id' => 200, 'host_id' => 200, 'title' => 'blah','type' => 'Music')); 566 + $e2 = Event::create(array('venue_id' => 200, 'host_id' => 200, 'title' => 'blah2','type' => 'Music')); 567 + 568 + $events = Event::find(array($e1->id, $e2->id), array('include' => 'venue')); 569 + 570 + foreach ($events as $e) 571 + $this->assert_null($e->venue); 572 + 573 + $this->assert_true(strpos(ActiveRecord\Table::load('Event')->last_sql, 'WHERE `id` IN(?,?)') !== false); 574 + $this->assert_true(strpos(ActiveRecord\Table::load('Venue')->last_sql, 'WHERE `id` IN(?,?)') !== false); 575 + } 576 + 577 + public function test_eager_loading_clones_related_objects() 578 + { 579 + $events = Event::find(array(2,3), array('include' => array('venue'))); 580 + 581 + $venue = $events[0]->venue; 582 + $venue->name = "new name"; 583 + 584 + $this->assert_equals($venue->id, $events[1]->venue->id); 585 + $this->assert_not_equals($venue->name, $events[1]->venue->name); 586 + $this->assert_not_equals(spl_object_hash($venue), spl_object_hash($events[1]->venue)); 587 + } 588 + 589 + public function test_eager_loading_clones_nested_related_objects() 590 + { 591 + $venues = Venue::find(array(1,2,6,9), array('include' => array('events' => array('host')))); 592 + 593 + $unchanged_host = $venues[2]->events[0]->host; 594 + $changed_host = $venues[3]->events[0]->host; 595 + $changed_host->name = "changed"; 596 + 597 + $this->assert_equals($changed_host->id, $unchanged_host->id); 598 + $this->assert_not_equals($changed_host->name, $unchanged_host->name); 599 + $this->assert_not_equals(spl_object_hash($changed_host), spl_object_hash($unchanged_host)); 600 + } 447 601 }; 448 602 ?>
+3 -1
lib/php-activerecord/test/fixtures/awesome_people.csv
··· 1 1 id,author_id 2 - 1,1 2 + 1,1 3 + 2,2 4 + 3,3
+2 -1
lib/php-activerecord/test/fixtures/books.csv
··· 1 1 book_id,author_id,secondary_author_id,name,special 2 - 1,1,2,"Ancient Art of Main Tanking",0 2 + 1,1,2,"Ancient Art of Main Tanking",0 3 + 2,2,2,"Another Book",0
+1
lib/php-activerecord/test/models/Author.php
··· 3 3 { 4 4 static $pk = 'author_id'; 5 5 static $has_one = array(array('awesome_person', 'foreign_key' => 'author_id', 'primary_key' => 'author_id')); 6 + static $has_many; 6 7 static $belongs_to = array(); 7 8 static $setters = array('password'); 8 9