| [ Index ] |
PHP Cross Reference of Akelos Framework |
[Summary view] [Print] [Text view]
1 <?php 2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ 3 4 // +----------------------------------------------------------------------+ 5 // | Akelos Framework - http://www.akelos.org | 6 // +----------------------------------------------------------------------+ 7 // | Copyright (c) 2002-2006, Akelos Media, S.L. & Bermi Ferrer Martinez | 8 // | Released under the GNU Lesser General Public License, see LICENSE.txt| 9 // +----------------------------------------------------------------------+ 10 11 /** 12 * @package ActiveRecord 13 * @subpackage Associations 14 * @author Bermi Ferrer <bermi a.t akelos c.om> 15 * @copyright Copyright (c) 2002-2006, Akelos Media, S.L. http://www.akelos.org 16 * @license GNU Lesser General Public License <http://www.gnu.org/copyleft/lesser.html> 17 */ 18 19 defined('AK_HAS_AND_BELONGS_TO_MANY_CREATE_JOIN_MODEL_CLASSES') ? null : define('AK_HAS_AND_BELONGS_TO_MANY_CREATE_JOIN_MODEL_CLASSES' ,true); 20 defined('AK_HAS_AND_BELONGS_TO_MANY_JOIN_CLASS_EXTENDS') ? null : define('AK_HAS_AND_BELONGS_TO_MANY_JOIN_CLASS_EXTENDS' , 'ActiveRecord'); 21 22 require_once (AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkAssociation.php'); 23 24 /** 25 * Associates two classes via an intermediate join table. Unless the join table is explicitly specified as 26 * an option, it is guessed using the lexical order of the class names. So a join between Developer and Project 27 * will give the default join table name of "developers_projects" because "D" outranks "P". 28 * 29 * Adds the following methods for retrieval and query. 30 * 'collection' is replaced with the associton identification passed as the first argument, so 31 * <tt>var $has_and_belongs_to_many = 'categories'</tt> would make available on the its parent an array of 32 * objects on $this->categories and a collection handling interface instance on $this->category (singular form) 33 * * <tt>collection->load($force_reload = false)</tt> - returns an array of all the associated objects. 34 * An empty array is returned if none is found. 35 * * <tt>collection->add($object, ...)</tt> - adds one or more objects to the collection by creating associations in the join table 36 * (collection->push and $collection->concat are aliases to this method). 37 * * <tt>collection->pushWithAttributes($object, $join_attributes)</tt> - adds one to the collection by creating an association in the join table that 38 * also holds the attributes from <tt>join_attributes</tt> (should be an array with the column names as keys). This can be used to have additional 39 * attributes on the join, which will be injected into the associated objects when they are retrieved through the collection. 40 * (collection->concatWithAttributes() is an alias to this method). 41 * * <tt>collection->delete($object, ...)</tt> - removes one or more objects from the collection by removing their associations from the join table. 42 * This does not destroy the objects. 43 * * <tt>collection->set($objects)</tt> - replaces the collections content by deleting and adding objects as appropriate. 44 * * <tt>collection->setByIds($ids)</tt> - replace the collection by the objects identified by the primary keys in $ids 45 * * <tt>collection->clear()</tt> - removes every object from the collection. This does not destroy the objects. 46 * * <tt>collection->isEmpty()</tt> - returns true if there are no associated objects. 47 * * <tt>collection->size()</tt> - returns the number of associated objects. (collection->count() is an alias to this method) 48 * * <tt>collection->find($id)</tt> - finds an associated object responding to the +id+ and that 49 * meets the condition that it has to be associated with this object. 50 * 51 * Example: A Developer class declares <tt>var $has_and_belongs_to_many = 'projects'</tt>, which will add: 52 * * <tt>Developer->projects</tt> (The collection) 53 * * <tt>Developer->project</tt> (The association manager) 54 * * <tt>Developer->project->add()<<</tt> 55 * * <tt>Developer->project->pushWithAttributes()</tt> 56 * * <tt>Developer->project->delete()</tt> 57 * * <tt>Developer->project->set()</tt> 58 * * <tt>Developer->project->setByIds()</tt> 59 * * <tt>Developer->project->clear()</tt> 60 * * <tt>Developer->projects->isEmpty()</tt> 61 * * <tt>Developer->projects->size()</tt> 62 * * <tt>Developer->projects->find($id)</tt> 63 * 64 * The declaration may include an options hash to specialize the behavior of the association. 65 * 66 * Options are: 67 * * <tt>class_name</tt> - specify the class name of the association. Use it only if that name can't be inferred 68 * from the association name. So <tt>var $has_and_belongs_to_many = 'projects'</tt> will by default be linked to the 69 * Project class, but if the real class name is SuperProject, you'll have to specify it with this option. 70 * * <tt>table_name</tt> - Name for the associated object database table. As this association will not instantiate the associated model 71 * it nees to know the name of the table we are going to join. This is infered form previous class_name 72 * * <tt>join_table</tt> - specify the name of the join table if the default based on lexical order isn't what you want. 73 * WARNING: If you're overwriting the table name of either class, the table_name method MUST be declared underneath any 74 * $has_and_belongs_to_many declaration in order to work. 75 * * <tt>join_class_name</tt> - specify the class name of the association join table. If the class does not exist, a new class 76 * will be created on runtime to load the results into the collection. The class name will be infered from the join_table name 77 * * <tt>foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name 78 * of this class in lower-case and "_id" suffixed. So a Person class that makes a $has_and_belongs_to_many association 79 * will use "person_id" as the default foreign_key. 80 * * <tt>association_foreign_key</tt> - specify the association foreign key used for the association. By default this is 81 * guessed to be the name of the associated class in lower-case and "_id" suffixed. So if the associated class is Project, 82 * the has_and_belongs_to_many association will use "project_id" as the default association foreign_key. 83 * * <tt>conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE" 84 * sql fragment, such as "authorized = 1". 85 * * <tt>order</tt> - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment, such as "last_name, first_name DESC" 86 * * <tt>finder_sql</tt> - overwrite the default generated SQL used to fetch the association with a manual one 87 * * <tt>delete_sql</tt> - overwrite the default generated SQL used to remove links between the associated 88 * classes with a manual one 89 * * <tt>insert_sql</tt> - overwrite the default generated SQL used to add links between the associated classes 90 * with a manual one 91 * * <tt>include</tt> - specify second-order associations that should be eager loaded when the collection is loaded. 92 * * <tt>group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. 93 * * <tt>limit</tt>: An integer determining the limit on the number of rows that should be returned. 94 * * <tt>offset</tt>: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. 95 * * <tt>select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not 96 * include the joined columns. 97 * * <tt>unique</tt> - if set to true, duplicates will be omitted from the collection. 98 * 99 * Option examples: 100 * $has_and_belongs_to_many = 'projects'; 101 * $has_and_belongs_to_many = array('projects'=> array('include' => array('milestones', 'manager'))) 102 * $has_and_belongs_to_many = array('nations' => array('class_name' => "Country")) 103 * $has_and_belongs_to_many = array('categories' => array('join_table' => "prods_cats")) 104 * $has_and_belongs_to_many = array('active_projects' => array('join_table' => 'developers_projects', 'delete_sql' => 105 * 'DELETE FROM developers_projects WHERE active=1 AND developer_id = $id AND project_id = $record->id' 106 */ 107 class AkHasAndBelongsToMany extends AkAssociation 108 { 109 /** 110 * Join object place holder 111 */ 112 var $JoinObject; 113 var $associated_ids = array(); 114 var $association_id; 115 var $_automatically_create_join_model_files = AK_HAS_AND_BELONGS_TO_MANY_CREATE_JOIN_MODEL_CLASSES; 116 117 function &addAssociated($association_id, $options = array()) 118 { 119 $default_options = array( 120 'class_name' => empty($options['class_name']) ? AkInflector::classify($association_id) : $options['class_name'], 121 'table_name' => false, 122 'join_table' => false, 123 'join_class_name' => false, 124 'foreign_key' => false, 125 'association_foreign_key' => false, 126 'conditions' => false, 127 'order' => false, 128 'join_class_extends' => AK_HAS_AND_BELONGS_TO_MANY_JOIN_CLASS_EXTENDS, 129 'join_class_primary_key' => 'id', // Used for removing items from the collection 130 'finder_sql' => false, 131 'delete_sql' => false, 132 'insert_sql' => false, 133 'include' => false, 134 'group' => false, 135 'limit' => false, 136 'offset' => false, 137 'handler_name' => strtolower(AkInflector::underscore(AkInflector::singularize($association_id))), 138 'select' => false, 139 'instantiate' => false, 140 'unique' => false 141 /* 142 'include_conditions_when_included' => true, 143 'include_order_when_included' => true, 144 'counter_sql' => false, 145 */ 146 147 ); 148 149 $options = array_merge($default_options, $options); 150 151 $owner_name = $this->Owner->getModelName(); 152 $owner_table = $this->Owner->getTableName(); 153 $associated_name = $options['class_name']; 154 $associated_table_name = $options['table_name'] = (empty($options['table_name']) ? AkInflector::tableize($associated_name) : $options['table_name']); 155 156 $join_tables = array($owner_table, $associated_table_name); 157 sort($join_tables); 158 159 $options['join_table'] = empty($options['join_table']) ? join('_', $join_tables) : $options['join_table']; 160 $options['join_class_name'] = empty($options['join_class_name']) ? join(array_map(array('AkInflector','classify'),array_map(array('AkInflector','singularize'), $join_tables))) : $options['join_class_name']; 161 $options['foreign_key'] = empty($options['foreign_key']) ? AkInflector::underscore($owner_name).'_id' : $options['foreign_key']; 162 $options['association_foreign_key'] = empty($options['association_foreign_key']) ? AkInflector::underscore($associated_name).'_id' : $options['association_foreign_key']; 163 164 $Collection =& $this->_setCollectionHandler($association_id, $options['handler_name']); 165 $Collection->setOptions($association_id, $options); 166 167 $this->addModel($association_id, $Collection); 168 169 if($options['instantiate']){ 170 $associated =& $Collection->load(); 171 } 172 173 $this->setAssociatedId($association_id, $options['handler_name']); 174 $Collection->association_id = $association_id; 175 176 $Collection->_loadJoinObject(); 177 178 return $Collection; 179 } 180 181 function getType() 182 { 183 return 'hasAndBelongsToMany'; 184 } 185 186 function &_setCollectionHandler($association_id, $handler_name) 187 { 188 $false = false; 189 if(isset($this->Owner->$association_id)){ 190 if(!is_array($this->Owner->$association_id)){ 191 trigger_error(Ak::t('%model_name::%association_id is not a collection array on current %association_id hasAndBelongsToMany association',array('%model_name'=>$this->Owner->getModelName(), '%association_id'=>$association_id)), E_USER_NOTICE); 192 } 193 $associated =& $this->Owner->$association_id; 194 }else{ 195 $associated = array(); 196 $this->Owner->$association_id =& $associated; 197 } 198 199 if(isset($this->Owner->$handler_name)){ 200 trigger_error(Ak::t('Could not load %association_id on %model_name because "%model_name->%handler_name" attribute '. 201 'is already defined and can\'t be used as an association placeholder', 202 array('%model_name'=>$this->Owner->getModelName(),'%association_id'=>$association_id, '%handler_name'=>$handler_name)), 203 E_USER_ERROR); 204 return $false; 205 }else{ 206 $this->Owner->$handler_name =& new AkHasAndBelongsToMany($this->Owner); 207 } 208 return $this->Owner->$handler_name; 209 } 210 211 212 // We are going to load or create the join class 213 function _loadJoinObject() 214 { 215 if($this->_isJoinObjectLoaded()){ 216 return true; 217 } 218 $options = $this->getOptions($this->association_id); 219 220 if (class_exists($options['join_class_name']) || $this->_loadJoinClass($options['join_class_name']) || $this->_createJoinClass()) { 221 $this->JoinObject =& new $options['join_class_name'](); 222 if($this->_tableExists($options['join_table']) || $this->_createJoinTable()){ 223 $this->JoinObject->setPrimaryKey($options['foreign_key']); 224 return true; 225 226 } else { 227 trigger_error(Ak::t('Could not find join table %table_name for hasAndBelongsToMany association %id',array('%table_name'=>$options['join_table'],'id'=>$this->association_id)),E_USER_ERROR); 228 } 229 } else { 230 trigger_error(Ak::t('Could not find join model %model_name for hasAndBelongsToMany association %id',array('%table_name'=>$options['join_class_name'],'id'=>$this->association_id)),E_USER_ERROR); return false; 231 } 232 return false; 233 } 234 235 function _isJoinObjectLoaded() 236 { 237 $options = $this->getOptions($this->association_id); 238 return !empty($this->JoinObject) && (strtolower($options['join_class_name']) == strtolower(get_class($this->JoinObject))); 239 } 240 241 function _loadJoinClass($class_name) 242 { 243 $model_file = AkInflector::toModelFilename($class_name); 244 return file_exists($model_file) && require_once($model_file); 245 } 246 247 function _createJoinClass() 248 { 249 $options = $this->getOptions($this->association_id); 250 251 $class_file_code = "<?php \n\n//This code was generated automatically by the active record hasAndBelongsToMany Method\n\n"; 252 $class_code = 253 "class {$options['join_class_name']} extends {$options['join_class_extends']} { 254 var \$_avoidTableNameValidation = true; 255 function {$options['join_class_name']}() 256 { 257 \$this->setModelName(\"{$options['join_class_name']}\"); 258 \$attributes = (array)func_get_args(); 259 \$this->setTableName('{$options['join_table']}', true, true); 260 \$this->init(\$attributes); 261 } 262 }"; 263 $class_file_code .= $class_code. "\n\n?>"; 264 $join_file = AkInflector::toModelFilename($options['join_class_name']); 265 if($this->_automatically_create_join_model_files && !file_exists($join_file) && @Ak::file_put_contents($join_file, $class_file_code)){ 266 require_once($join_file); 267 }else{ 268 eval($class_code); 269 } 270 return class_exists($options['join_class_name']); 271 } 272 273 function _tableExists($table_name) 274 { 275 return $this->JoinObject->setTableName($table_name, true, true); 276 } 277 278 function _createJoinTable() 279 { 280 $options = $this->getOptions($this->association_id); 281 require_once (AK_LIB_DIR.DS.'AkInstaller.php'); 282 $Installer =& new AkInstaller(); 283 $Installer->createTable($options['join_table'],"id,{$options['foreign_key']},{$options['association_foreign_key']}",array('timestamp'=>false)); 284 return $this->JoinObject->setTableName($options['join_table'],false); 285 } 286 287 function &load($force_reload = false) 288 { 289 $options = $this->getOptions($this->association_id); 290 if($force_reload || empty($this->Owner->{$options['handler_name']}->_loaded)){ 291 if(!$this->Owner->isNewRecord()){ 292 $this->constructSql(); 293 $options = $this->getOptions($this->association_id); 294 $Associated =& $this->getAssociatedModelInstance(); 295 if($FoundAssociates = $Associated->findBySql($options['finder_sql'])){ 296 array_map(array(&$this,'_setAssociatedMemberId'),$FoundAssociates); 297 $this->Owner->{$this->association_id} =& $FoundAssociates; 298 } 299 } 300 if(empty($this->Owner->{$this->association_id})){ 301 $this->Owner->{$this->association_id} = array(); 302 } 303 304 $this->Owner->{$options['handler_name']}->_loaded = true; 305 } 306 return $this->Owner->{$this->association_id}; 307 } 308 309 310 /** 311 * add($object), add(array($object, $object2)) - adds one or more objects to the collection by setting 312 * their foreign keys to the collection?s primary key. Items are saved automatically when parent has been saved. 313 */ 314 function add(&$Associated) 315 { 316 if(is_array($Associated)){ 317 $external_key = '__associated_to_model_'.$this->Owner->getModelName().'_as_'.$this->association_id; 318 $succes = true; 319 $succes = $this->Owner->notifyObservers('beforeAdd') ? $succes : false; 320 $options = $this->getOptions($this->association_id); 321 foreach (array_keys($Associated) as $k){ 322 if(empty($Associated[$k]->$external_key) && !$this->_hasAssociatedMember($Associated[$k])){ 323 if(!empty($options['before_add']) && method_exists($this->Owner, $options['before_add']) && $this->Owner->{$options['before_add']}($Associated[$k]) === false ){ 324 $succes = false; 325 }else{ 326 $Associated[$k]->$external_key = $Associated[$k]->isNewRecord(); 327 $this->Owner->{$this->association_id}[] =& $Associated[$k]; 328 $this->_setAssociatedMemberId($Associated[$k]); 329 if($this->_relateAssociatedWithOwner($Associated[$k])){ 330 if($succes && !empty($options['after_add']) && method_exists($this->Owner, $options['after_add']) && $this->Owner->{$options['after_add']}($Associated[$k]) === false ){ 331 $succes = false; 332 } 333 }else{ 334 $succes = false; 335 } 336 } 337 } 338 } 339 $succes = $this->Owner->notifyObservers('afterAdd') ? $succes : false; 340 return $succes; 341 }else{ 342 $associates = array(); 343 $associates[] =& $Associated; 344 return $this->add($associates); 345 } 346 } 347 348 349 function push(&$record) 350 { 351 return $this->add($record); 352 } 353 354 function concat(&$record) 355 { 356 return $this->add($record); 357 } 358 359 /** 360 * Remove all records from this association 361 */ 362 function deleteAll() 363 { 364 $this->load(); 365 return $this->delete($this->Owner->{$this->association_id}); 366 } 367 368 function reset() 369 { 370 $options = $this->getOptions($this->association_id); 371 $this->Owner->{$options['handler_name']}->_loaded = false; 372 } 373 374 function set(&$objects) 375 { 376 $this->deleteAll($objects); 377 $this->add($objects); 378 } 379 380 function setIds() 381 { 382 $ids = func_get_args(); 383 $ids = is_array($ids[0]) ? $ids[0] : $ids; 384 $AssociatedModel =& $this->getAssociatedModelInstance(); 385 if(!empty($ids)){ 386 $NewAssociates =& $AssociatedModel->find($ids); 387 $this->set($NewAssociates); 388 } 389 } 390 391 function setByIds() 392 { 393 $ids = func_get_args(); 394 call_user_func_array(array($this,'setIds'), $ids); 395 } 396 397 function addId($id) 398 { 399 $AssociatedModel =& $this->getAssociatedModelInstance(); 400 if($NewAssociated =& $AssociatedModel->find($id)){ 401 return $this->add($NewAssociated); 402 } 403 return false; 404 } 405 406 407 function delete(&$Associated) 408 { 409 $success = true; 410 if(!is_array($Associated)){ 411 $associated_elements = array(); 412 $associated_elements[] =& $Associated; 413 return $this->delete($associated_elements); 414 }else{ 415 $options = $this->getOptions($this->association_id); 416 417 $ids_to_nullify = array(); 418 $ids_to_delete = array(); 419 $items_to_remove_from_collection = array(); 420 $AssociatedModel =& $this->getAssociatedModelInstance(); 421 422 $owner_type = $this->_findOwnerTypeForAssociation($AssociatedModel, $this->Owner); 423 424 $this->JoinObject->setPrimaryKey($options['join_class_primary_key']); 425 426 foreach (array_keys($Associated) as $k){ 427 $id = $Associated[$k]->getId(); 428 if($JoinObjectsToDelete =& $this->JoinObject->findAllBy($options['foreign_key'].' AND '.$options['association_foreign_key'], $this->Owner->getId(), $id)){ 429 foreach (array_keys($JoinObjectsToDelete) as $k) { 430 if($JoinObjectsToDelete[$k]->destroy()){ 431 $items_to_remove_from_collection[] = $id; 432 }else{ 433 $success = false; 434 } 435 } 436 } 437 } 438 439 $this->JoinObject->setPrimaryKey($options['foreign_key']); 440 441 $success ? $this->removeFromCollection($items_to_remove_from_collection) : null; 442 } 443 444 return $success; 445 } 446 447 448 449 /** 450 * Remove records from the collection. Use delete() in order to trigger database dependencies 451 */ 452 function removeFromCollection(&$records) 453 { 454 if(!is_array($records)){ 455 $records_array = array(); 456 $records_array[] =& $records; 457 $this->delete($records_array); 458 }else{ 459 $this->Owner->notifyObservers('beforeRemove'); 460 $options = $this->getOptions($this->association_id); 461 foreach (array_keys($records) as $k){ 462 if(!empty($options['before_remove']) && method_exists($this->Owner, $options['before_remove']) && $this->Owner->{$options['before_remove']}($records[$k]) === false ){ 463 continue; 464 } 465 466 if(isset($records[$k]->__activeRecordObject)){ 467 $record_id = $records[$k]->getId(); 468 }else{ 469 $record_id = $records[$k]; 470 } 471 472 foreach (array_keys($this->Owner->{$this->association_id}) as $kk){ 473 if( 474 ( 475 !empty($this->Owner->{$this->association_id}[$kk]->__hasAndBelongsToManyMemberId) && 476 !empty($records[$k]->__hasAndBelongsToManyMemberId) && 477 $records[$k]->__hasAndBelongsToManyMemberId == $this->Owner->{$this->association_id}[$kk]->__hasAndBelongsToManyMemberId 478 ) || ( 479 !empty($this->Owner->{$this->association_id}[$kk]->__activeRecordObject) && 480 $record_id == $this->Owner->{$this->association_id}[$kk]->getId() 481 ) 482 ){ 483 unset($this->Owner->{$this->association_id}[$kk]); 484 } 485 } 486 unset($this->associated_ids[$record_id]); 487 $this->_unsetAssociatedMemberId($records[$k]); 488 if(!empty($options['after_remove']) && method_exists($this->Owner, $options['after_remove'])){ 489 $this->Owner->{$options['after_remove']}($records[$k]); 490 } 491 } 492 $this->Owner->notifyObservers('afterRemove'); 493 } 494 } 495 496 497 498 499 function _setAssociatedMemberId(&$Member) 500 { 501 if(empty($Member->__hasAndBelongsToManyMemberId)) { 502 $Member->__hasAndBelongsToManyMemberId = Ak::randomString(); 503 } 504 $object_id = $Member->getId(); 505 if(!empty($object_id)){ 506 $this->associated_ids[$object_id] = $Member->__hasAndBelongsToManyMemberId; 507 } 508 } 509 510 function _unsetAssociatedMemberId(&$Member) 511 { 512 $id = $this->_getAssociatedMemberId($Member); 513 unset($this->associated_ids[$id]); 514 unset($Member->__hasAndBelongsToManyMemberId); 515 } 516 517 function _getAssociatedMemberId(&$Member) 518 { 519 if(!empty($Member->__hasAndBelongsToManyMemberId)) { 520 return array_search($Member->__hasAndBelongsToManyMemberId, $this->associated_ids); 521 } 522 return false; 523 } 524 525 function _hasAssociatedMember(&$Member) 526 { 527 $options = $this->getOptions($this->association_id); 528 if($options['unique'] && !$Member->isNewRecord() && isset($this->associated_ids[$Member->getId()])){ 529 return true; 530 } 531 $id = $this->_getAssociatedMemberId($Member); 532 return !empty($id); 533 } 534 535 536 function _relateAssociatedWithOwner(&$Associated) 537 { 538 if(!$this->Owner->isNewRecord()){ 539 $success = true; 540 $options = $this->getOptions($this->association_id); 541 if(strtolower($options['join_class_name']) != strtolower(get_class($this->JoinObject))){ 542 return false; 543 } 544 if($Associated->isNewRecord() ? $Associated->save() : true){ 545 if(!$this->_getAssociatedMemberId($Associated)){ 546 $this->_setAssociatedMemberId($Associated); 547 } 548 549 $foreign_key = $this->Owner->getId(); 550 $association_foreign_key = $Associated->getId(); 551 if($foreign_key != $this->JoinObject->get($options['foreign_key']) || 552 $association_foreign_key != $this->JoinObject->get($options['association_foreign_key'])){ 553 554 $this->JoinObject =& $this->JoinObject->create(array($options['foreign_key']=> $foreign_key, $options['association_foreign_key']=> $association_foreign_key)); 555 $success = !$this->JoinObject->isNewRecord(); 556 } 557 } 558 if($success){ 559 $Associated->hasAndBelongsToMany->__joined = true; 560 } 561 return $success; 562 } 563 return false; 564 } 565 566 function &_build($association_id, &$AssociatedObject, $reference_associated = true) 567 { 568 if($reference_associated){ 569 $this->Owner->$association_id =& $AssociatedObject; 570 }else{ 571 $this->Owner->$association_id = $AssociatedObject; 572 } 573 $this->Owner->$association_id->_AssociationHandler =& $this; 574 $this->Owner->$association_id->_associatedAs = $this->getType(); 575 $this->Owner->$association_id->_associationId = $association_id; 576 $this->Owner->_associations[$association_id] =& $this->Owner->$association_id; 577 return $this->Owner->$association_id; 578 } 579 580 581 582 583 function constructSql() 584 { 585 $options = $this->getOptions($this->association_id); 586 if(empty($options['finder_sql'])){ 587 $is_sqlite = $this->Owner->_db->type() == 'sqlite'; 588 $options['finder_sql'] = "SELECT {$options['table_name']}.* FROM {$options['table_name']} ". 589 $this->associationJoin(). 590 "WHERE ".$this->Owner->getTableName().'.'.$this->Owner->getPrimaryKey()." ". 591 ($is_sqlite ? ' LIKE ' : ' = ').' '.$this->Owner->quotedId(); // (HACK FOR SQLITE) Otherwise returns wrong data 592 $options['finder_sql'] .= !empty($options['conditions']) ? ' AND '.$options['conditions'].' ' : ''; 593 $options['finder_sql'] .= !empty($options['conditions']) ? ' AND '.$options['conditions'].' ' : ''; 594 } 595 if(empty($options['counter_sql'])){ 596 $options['counter_sql'] = substr_replace($options['finder_sql'],'SELECT COUNT(*)',0,strpos($options['finder_sql'],'*')+1); 597 } 598 $this->setOptions($this->association_id, $options); 599 } 600 601 function associationJoin() 602 { 603 $Associated =& $this->getAssociatedModelInstance(); 604 $options = $this->getOptions($this->association_id); 605 606 return "LEFT OUTER JOIN {$options['join_table']} ON ". 607 "{$options['join_table']}.{$options['association_foreign_key']} = {$options['table_name']}.".$Associated->getPrimaryKey()." ". 608 "LEFT OUTER JOIN ".$this->Owner->getTableName()." ON ". 609 "{$options['join_table']}.{$options['foreign_key']} = ".$this->Owner->getTableName().".".$this->Owner->getPrimaryKey()." "; 610 } 611 612 613 614 function count() 615 { 616 $count = 0; 617 $options = $this->getOptions($this->association_id); 618 if(empty($this->Owner->{$options['handler_name']}->_loaded) && !$this->Owner->isNewRecord()){ 619 $this->constructSql(); 620 $options = $this->getOptions($this->association_id); 621 $Associated =& $this->getAssociatedModelInstance(); 622 623 if($this->_hasCachedCounter()){ 624 $count = $Associated->getAttribute($this->_getCachedCounterAttributeName()); 625 }elseif(!empty($options['counter_sql'])){ 626 $count = $Associated->countBySql($options['counter_sql']); 627 }else{ 628 $count = (strtoupper(substr($options['finder_sql'],0,6)) != 'SELECT') ? 629 $Associated->count($options['foreign_key'].'='.$this->Owner->quotedId()) : 630 $Associated->countBySql($options['finder_sql']); 631 } 632 }else{ 633 $count = count($this->Owner->{$this->association_id}); 634 } 635 636 if($count == 0){ 637 $this->Owner->{$this->association_id} = array(); 638 $this->Owner->{$options['handler_name']}->_loaded = true; 639 } 640 641 return $count; 642 } 643 644 645 function &build($attributes = array(), $set_as_new_record = true) 646 { 647 $options = $this->getOptions($this->association_id); 648 Ak::import($options['class_name']); 649 $record =& new $options['class_name']($attributes); 650 $record->_newRecord = $set_as_new_record; 651 $this->Owner->{$this->association_id}[] =& $record; 652 $this->_setAssociatedMemberId($record); 653 $set_as_new_record ? $this->_relateAssociatedWithOwner($record) : null; 654 return $record; 655 } 656 657 function &create($attributes = array()) 658 { 659 $record =& $this->build($attributes); 660 if(!$this->Owner->isNewRecord()){ 661 $record->save(); 662 } 663 return $record; 664 } 665 666 667 function getAssociatedFinderSqlOptions($association_id, $options = array()) 668 { 669 $options = $this->getOptions($this->association_id); 670 $Associated =& $this->getAssociatedModelInstance(); 671 $table_name = $Associated->getTableName(); 672 $owner_id = $this->Owner->quotedId(); 673 674 $finder_options = array(); 675 676 foreach ($options as $option=>$value) { 677 if(!empty($value)){ 678 $finder_options[$option] = trim($Associated->_addTableAliasesToAssociatedSql('_'.$this->association_id, $value)); 679 } 680 } 681 682 $finder_options['joins'] = $this->constructSqlForInclusion(); 683 $finder_options['selection'] = ''; 684 685 foreach (array_keys($Associated->getColumns()) as $column_name){ 686 $finder_options['selection'] .= '_'.$this->association_id.'.'.$column_name.' AS _'.$this->association_id.'_'.$column_name.', '; 687 } 688 689 $finder_options['selection'] = trim($finder_options['selection'], ', '); 690 691 /** 692 * @todo Refactorize me. This is too confusing 693 */ 694 $finder_options['conditions'] = 695 // We add previous conditions 696 (!empty($options['conditions']) ? 697 ' AND '.$Associated->_addTableAliasesToAssociatedSql('_'.$this->association_id, $options['conditions']).' ' : ''); 698 699 return $finder_options; 700 } 701 702 function constructSqlForInclusion() 703 { 704 $Associated =& $this->getAssociatedModelInstance(); 705 $options = $this->getOptions($this->association_id); 706 return 707 ' LEFT OUTER JOIN '. 708 $options['join_table'].' AS _'.$options['join_class_name']. 709 ' ON '. 710 '__owner.'.$this->Owner->getPrimaryKey(). 711 ' = '. 712 '_'.$options['join_class_name'].'.'.$options['foreign_key']. 713 714 ' LEFT OUTER JOIN '. 715 $options['table_name'].' AS _'.$this->association_id. 716 ' ON '. 717 '_'.$this->association_id.'.'.$Associated->getPrimaryKey(). 718 ' = '. 719 '_'.$options['join_class_name'].'.'.$options['association_foreign_key'].' '; 720 721 } 722 723 724 function _hasCachedCounter() 725 { 726 $Associated =& $this->getAssociatedModelInstance(); 727 return $Associated->isAttributePresent($this->_getCachedCounterAttributeName()); 728 } 729 730 function _getCachedCounterAttributeName() 731 { 732 return $this->association_id.'_count'; 733 } 734 735 736 function &getAssociatedModelInstance() 737 { 738 static $ModelInstances; 739 $class_name = $this->getOption($this->association_id, 'class_name'); 740 if(empty($ModelInstances[$class_name])){ 741 Ak::import($class_name); 742 $ModelInstances[$class_name] =& new $class_name(); 743 } 744 return $ModelInstances[$class_name]; 745 } 746 747 748 function find() 749 { 750 $result = false; 751 if(!$this->Owner->isNewRecord()){ 752 $this->constructSql(); 753 $has_and_belongs_to_many_options = $this->getOptions($this->association_id); 754 $Associated =& $this->getAssociatedModelInstance(); 755 756 $args = func_get_args(); 757 $num_args = func_num_args(); 758 759 if(!empty($args[$num_args-1]) && is_array($args[$num_args-1])){ 760 $options_in_args = true; 761 $options = $args[$num_args-1]; 762 }else{ 763 $options_in_args = false; 764 $options = array(); 765 } 766 767 $options['conditions'] = empty($options['conditions']) ? @$has_and_belongs_to_many_options['finder_sql'] : 768 (empty($has_and_belongs_to_many_options['finder_sql']) || strstr($options['conditions'], $has_and_belongs_to_many_options['finder_sql']) 769 ? $options['conditions'] : $options['conditions'].' AND '.$has_and_belongs_to_many_options['finder_sql']); 770 771 $options['order'] = empty($options['order']) ? @$has_and_belongs_to_many_options['order'] : $options['order']; 772 773 $options['select_prefix'] = ''; 774 775 if($options_in_args){ 776 $args[$num_args-1] = $options; 777 }else{ 778 $args = empty($args) ? array('all') : $args; 779 array_push($args, $options); 780 } 781 782 $result =& Ak::call_user_func_array(array(&$Associated,'find'), $args); 783 } 784 785 return $result; 786 } 787 788 789 function isEmpty() 790 { 791 return $this->count() === 0; 792 } 793 794 function getSize() 795 { 796 return $this->count(); 797 } 798 799 function clear() 800 { 801 return $this->deleteAll(); 802 } 803 804 /** 805 * Triggers 806 */ 807 function afterCreate(&$object) 808 { 809 return $this->_afterCallback($object); 810 } 811 812 function afterUpdate(&$object) 813 { 814 return $this->_afterCallback($object); 815 } 816 817 818 function beforeDestroy(&$object) 819 { 820 $success = true; 821 822 foreach ((array)$object->_associationIds as $k => $v){ 823 if(isset($object->$k) && is_array($object->$k) && isset($object->$v) && method_exists($object->$v, 'getType') && $object->$v->getType() == 'hasAndBelongsToMany'){ 824 $object->$v->load(); 825 $success = $object->$v->deleteAll() ? $success : false; 826 } 827 } 828 829 return $success; 830 } 831 832 function _afterCallback(&$object) 833 { 834 static $joined_items = array(); 835 $success = true; 836 837 $object_id = $object->getId(); 838 foreach (array_keys($object->hasAndBelongsToMany->models) as $association_id){ 839 $CollectionHandler =& $object->hasAndBelongsToMany->models[$association_id]; 840 $options = $CollectionHandler->getOptions($association_id); 841 $class_name = strtolower($CollectionHandler->getOption($association_id, 'class_name')); 842 843 if(!empty($object->$association_id) && is_array($object->$association_id)){ 844 $this->_removeDuplicates($object, $association_id); 845 foreach (array_keys($object->$association_id) as $k){ 846 847 if(!empty($object->{$association_id}[$k]) && strtolower(get_class($object->{$association_id}[$k])) == $class_name){ 848 $AssociatedItem =& $object->{$association_id}[$k]; 849 // This helps avoiding double realation on first time savings 850 851 852 if(!in_array($AssociatedItem->__hasAndBelongsToManyMemberId, $joined_items)){ 853 $joined_items[] = $AssociatedItem->__hasAndBelongsToManyMemberId; 854 855 if(empty($AssociatedItem->hasAndBelongsToMany->__joined) && ($AssociatedItem->isNewRecord()? $AssociatedItem->save() : true)){ 856 $AssociatedItem->hasAndBelongsToMany->__joined = true; 857 $CollectionHandler->JoinObject =& $CollectionHandler->JoinObject->create(array($options['foreign_key'] => $object_id ,$options['association_foreign_key'] => $AssociatedItem->getId())); 858 859 860 $success = !$CollectionHandler->JoinObject->isNewRecord() ? $success : false; 861 862 }else{ 863 $success = false; 864 } 865 } 866 } 867 } 868 } 869 if(!$success){ 870 return $success; 871 } 872 } 873 return $success; 874 } 875 876 function _removeDuplicates(&$object, $association_id) 877 { 878 if(!empty($object->{$association_id})){ 879 $CollectionHandler =& $object->hasAndBelongsToMany->models[$association_id]; 880 $options = $CollectionHandler->getOptions($association_id); 881 if(empty($options['unique'])){ 882 return ; 883 } 884 if($object->isNewRecord()){ 885 $ids = array(); 886 }else{ 887 if($existing = $CollectionHandler->find()){ 888 $ids = $existing[0]->collect($existing,'id','id'); 889 }else{ 890 $ids = array(); 891 } 892 } 893 $class_name = strtolower($CollectionHandler->getOption($association_id, 'class_name')); 894 foreach (array_keys($object->$association_id) as $k){ 895 if(!empty($object->{$association_id}[$k]) && strtolower(get_class($object->{$association_id}[$k])) == $class_name && !$object->{$association_id}[$k]->isNewRecord()){ 896 $AssociatedItem =& $object->{$association_id}[$k]; 897 if(isset($ids[$AssociatedItem->getId()])){ 898 unset($object->{$association_id}[$k]); 899 continue; 900 } 901 $ids[$AssociatedItem->getId()] = true; 902 } 903 } 904 } 905 } 906 } 907 908 909 ?>
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Mon Oct 27 12:43:49 2008 | Cross-referenced by PHPXref 0.6 |