2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
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 // +----------------------------------------------------------------------+
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>
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');
22 require_once(AK_LIB_DIR
.DS
.'AkActiveRecord'.DS
.'AkAssociation.php');
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".
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($bjects)</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.
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>
64 * The declaration may include an options hash to specialize the behavior of the association.
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
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.
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'
107 class AkHasAndBelongsToMany
extends AkAssociation
110 * Join object place holder
113 var $asssociated_ids = array();
115 var $_automatically_create_join_model_files = AK_HAS_AND_BELONGS_TO_MANY_CREATE_JOIN_MODEL_CLASSES
;
117 function &addAssociated($association_id, $options = array())
119 $default_options = array(
120 'class_name' => empty($options['class_name']) ? AkInflector
::modulize($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,
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,
137 'handler_name' => strtolower(AkInflector
::underscore(AkInflector
::singularize($association_id))),
139 'instantiate' => false,
142 'include_conditions_when_included' => true,
143 'include_order_when_included' => true,
144 'counter_sql' => false,
149 $options = array_merge($default_options, $options);
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']);
156 $join_tables = array($owner_table, $associated_table_name);
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','modulize'),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'];
164 $Collection =& $this->_setCollectionHandler($association_id, $options['handler_name']);
165 $Collection->setOptions($association_id, $options);
167 $this->addModel($association_id, $Collection);
169 if($options['instantiate']){
170 $associated =& $Collection->load();
173 $this->setAssociatedId($association_id, $options['handler_name']);
174 $Collection->association_id
= $association_id;
176 $Collection->_loadJoinObject() ?
null : trigger_error(Ak
::t('Could find join model %model_name for hasAndBelongsToMany association %id',array('%table_name'=>$options['join_class_name'],'id'=>$this->association_id
)),E_USER_ERROR
);
183 return 'hasAndBelongsToMany';
186 function &_setCollectionHandler($association_id, $handler_name)
188 if(isset($this->Owner
->$association_id)){
189 if(!is_array($this->Owner
->$association_id)){
190 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 $associated =& $this->Owner
->$association_id;
194 $associated = array();
195 $this->Owner
->$association_id =& $associated;
198 if(isset($this->Owner
->$handler_name)){
199 trigger_error(Ak
::t('Could not load %association_id on %model_name because "%model_name->%handler_name" attribute '.
200 'is already defided and can\' be used as an association placeholder',
201 array('%model_name'=>$this->Owner
->getModelName(),'%association_id'=>$association_id, '%handler_name'=>$handler_name)),
205 $this->Owner
->$handler_name =& new AkHasAndBelongsToMany($this->Owner
);
207 return $this->Owner
->$handler_name;
211 // We are going to load or create the join class
212 function _loadJoinObject()
214 if($this->_isJoinObjectLoaded()){
217 $options = $this->getOptions($this->association_id
);
219 $join_model_file = AkInflector
::toModelFilename($options['join_class_name']);
220 if(file_exists($join_model_file)){
221 require_once($join_model_file);
222 if(class_exists($options['join_class_name'])){
223 $this->JoinObject
=& new $options['join_class_name']();
224 $this->JoinObject
->setPrimaryKey($options['foreign_key']);
228 if($this->_createJoinClass()){
229 $this->JoinObject
=& new $options['join_class_name']();
230 if(!$this->_hasJoinTable()){
231 $this->_createJoinTable() ?
null : 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
);
233 $this->JoinObject
->setPrimaryKey($options['foreign_key']);
239 function _isJoinObjectLoaded()
241 $options = $this->getOptions($this->association_id
);
242 return !empty($this->JoinObject
) && (strtolower($options['join_class_name']) == strtolower(get_class($this->JoinObject
)));
244 function _createJoinClass()
246 $options = $this->getOptions($this->association_id
);
247 if(!class_exists($options['join_class_name'])){
248 $class_file_code = "<?php \n\n//This code was generated automatically by the active record hasAndBelongsToMany Method\n\n";
250 "class {$options['join_class_name']} extends {$options['join_class_extends']} {
251 var \$_avoidTableNameValidation = true;
252 function {$options['join_class_name']}()
254 \$this->setModelName(\"{$options['join_class_name']}\");
255 \$attributes = (array)func_get_args();
256 \$this->setTableName('{$options['join_table']}', true, true);
257 \$this->init(\$attributes);
260 $class_file_code .= $class_code. "\n\n?>";
261 $join_file = AkInflector
::toModelFilename($options['join_class_name']);
262 if($this->_automatically_create_join_model_files
&& !file_exists($join_file) && @Ak
::file_put_contents($join_file, $class_file_code)){
263 require_once($join_file);
268 return class_exists($options['join_class_name']);
271 function _hasJoinTable()
273 $options = $this->getOptions($this->association_id
);
274 if(isset($this->JoinObject
)){
275 return $this->JoinObject
->setTableName($options['join_table'], true, true);
280 function _createJoinTable()
282 $options = $this->getOptions($this->association_id
);
283 require_once(AK_LIB_DIR
.DS
.'AkDbManager.php');
285 AkDbManager
::createTable($options['join_table'], "id I AUTO KEY,{$options['foreign_key']} I, {$options['association_foreign_key']} I",array('mysql' => 'TYPE=InnoDB'),false,
286 "{$options['foreign_key']},{$options['association_foreign_key']}");
287 return $this->_hasJoinTable();
292 function &load($force_reload = false)
294 $options = $this->getOptions($this->association_id
);
295 if($force_reload ||
empty($this->Owner
->{$options['handler_name']}->_loaded
)){
296 if(!$this->Owner
->isNewRecord()){
297 $this->constructSql();
298 $options = $this->getOptions($this->association_id
);
299 $Associated =& $this->getAssociatedModelInstance();
300 if($FoundAssociates = $Associated->findBySql($options['finder_sql'])){
301 array_map(array(&$this,'_setAssociatedMemberId'),$FoundAssociates);
302 $this->Owner
->{$this->association_id
} =& $FoundAssociates;
305 if(empty($this->Owner
->{$this->association_id
})){
306 $this->Owner
->{$this->association_id
} = array();
309 $this->Owner
->{$options['handler_name']}->_loaded
= true;
311 return $this->Owner
->{$this->association_id
};
316 * add($object), add(array($object, $object2)) - adds one or more objects to the collection by setting
317 * their foreign keys to the collection?s primary key. Items are saved automatically when parent has been saved.
319 function add(&$Associated)
321 if(is_array($Associated)){
322 $external_key = '__associated_to_model_'.$this->Owner
->getModelName().'_as_'.$this->association_id
;
324 $succes = $this->Owner
->notifyObservers('beforeAdd') ?
$succes : false;
325 $options = $this->getOptions($this->association_id
);
326 foreach (array_keys($Associated) as $k){
327 if(empty($Associated[$k]->$external_key) && !$this->_hasAssociatedMember($Associated[$k])){
328 if(!empty($options['before_add']) && method_exists($this->Owner
, $options['before_add']) && $this->Owner
->{$options['before_add']}($Associated[$k]) === false ){
331 $Associated[$k]->$external_key = $Associated[$k]->isNewRecord();
332 $this->Owner
->{$this->association_id
}[] =& $Associated[$k];
333 $this->_setAssociatedMemberId($Associated[$k]);
334 if($this->_relateAssociatedWithOwner($Associated[$k])){
335 if($succes && !empty($options['after_add']) && method_exists($this->Owner
, $options['after_add']) && $this->Owner
->{$options['after_add']}($Associated[$k]) === false ){
344 $succes = $this->Owner
->notifyObservers('afterAdd') ?
$succes : false;
347 $associates = array();
348 $associates[] =& $Associated;
349 return $this->add($associates);
354 function push(&$record)
356 return $this->add($record);
359 function concat(&$record)
361 return $this->add($record);
365 * Remove all records from this association
367 function deleteAll($Skip = null)
370 return $this->delete($this->Owner
->{$this->association_id
}, $Skip);
375 $options = $this->getOptions($this->association_id
);
376 $this->Owner
->{$options['handler_name']}->_loaded
= false;
379 function set(&$objects)
381 $this->deleteAll($objects);
382 $this->add($objects);
387 $ids = func_get_args();
388 $ids = is_array($ids[0]) ?
$ids[0] : $ids;
389 $AssociatedModel =& $this->getAssociatedModelInstance();
391 $NewAssociates =& $AssociatedModel->find($ids);
392 $this->set($NewAssociates);
398 $ids = func_get_args();
399 call_user_func_array(array($this,'setIds'), $ids);
404 $AssociatedModel =& $this->getAssociatedModelInstance();
405 if($NewAssociated =& $AssociatedModel->find($id)){
406 return $this->add($NewAssociated);
412 function delete(&$Associated, $Skip = null)
415 if(!is_array($Associated)){
416 $associated_elements = array();
417 $associated_elements[] =& $Associated;
418 return $this->delete($associated_elements, $Skip);
420 $options = $this->getOptions($this->association_id
);
422 $ids_to_skip = array();
423 $Skip = empty($Skip) ?
null : (is_array($Skip) ?
$Skip : array($Skip));
425 foreach (array_keys($Skip) as $k){
426 $ids_to_skip[] = $Skip[$k]->getId();
430 $ids_to_nullify = array();
431 $ids_to_delete = array();
432 $items_to_remove_from_collection = array();
433 $AssociatedModel =& $this->getAssociatedModelInstance();
435 $owner_type = $this->_findOwnerTypeForAssociation($AssociatedModel, $this->Owner
);
437 $this->JoinObject
->setPrimaryKey($options['join_class_primary_key']);
439 foreach (array_keys($Associated) as $k){
440 $id = $Associated[$k]->getId();
442 if(!in_array($id, $ids_to_skip)){
444 if($JoinObjectsToDelete =& $this->JoinObject
->findAllBy($options['foreign_key'].' AND '.$options['association_foreign_key'], $this->Owner
->getId(), $id)){
445 foreach (array_keys($JoinObjectsToDelete) as $k) {
446 if($JoinObjectsToDelete[$k]->destroy()){
447 $items_to_remove_from_collection[] = $id;
456 $this->JoinObject
->setPrimaryKey($options['foreign_key']);
458 $success ?
$this->removeFromCollection($items_to_remove_from_collection) : null;
467 * Remove records from the collection. Use delete() in order to trigger database dependencies
469 function removeFromCollection(&$records)
471 if(!is_array($records)){
472 $records_array = array();
473 $records_array[] =& $records;
474 $this->delete($records_array);
476 $this->Owner
->notifyObservers('beforeRemove');
477 $options = $this->getOptions($this->association_id
);
478 foreach (array_keys($records) as $k){
479 if(!empty($options['before_remove']) && method_exists($this->Owner
, $options['before_remove']) && $this->Owner
->{$options['before_remove']}($records[$k]) === false ){
483 if(isset($records[$k]->__activeRecordObject
)){
484 $record_id = $records[$k]->getId();
486 $record_id = $records[$k];
489 foreach (array_keys($this->Owner
->{$this->association_id
}) as $kk){
492 !empty($this->Owner
->{$this->association_id
}[$kk]->__hasAndBelongsToManyMemberId
) &&
493 !empty($records[$k]->__hasAndBelongsToManyMemberId
) &&
494 $records[$k]->__hasAndBelongsToManyMemberId
== $this->Owner
->{$this->association_id
}[$kk]->__hasAndBelongsToManyMemberId
496 !empty($this->Owner
->{$this->association_id
}[$kk]->__activeRecordObject
) &&
497 $record_id == $this->Owner
->{$this->association_id
}[$kk]->getId()
500 unset($this->Owner
->{$this->association_id
}[$kk]);
503 unset($this->asssociated_ids
[$record_id]);
504 $this->_unsetAssociatedMemberId($records[$k]);
505 if(!empty($options['after_remove']) && method_exists($this->Owner
, $options['after_remove'])){
506 $this->Owner
->{$options['after_remove']}($records[$k]);
509 $this->Owner
->notifyObservers('afterRemove');
516 function _setAssociatedMemberId(&$Member)
518 if(empty($Member->__hasAndBelongsToManyMemberId
)) {
519 $Member->__hasAndBelongsToManyMemberId
= Ak
::randomString();
521 $object_id = $Member->getId();
522 if(!empty($object_id)){
523 $this->asssociated_ids
[$object_id] = $Member->__hasAndBelongsToManyMemberId
;
527 function _unsetAssociatedMemberId(&$Member)
529 $id = $this->_getAssociatedMemberId($Member);
530 unset($this->asssociated_ids
[$id]);
531 unset($Member->__hasAndBelongsToManyMemberId
);
534 function _getAssociatedMemberId(&$Member)
536 if(!empty($Member->__hasAndBelongsToManyMemberId
)) {
537 return array_search($Member->__hasAndBelongsToManyMemberId
, $this->asssociated_ids
);
542 function _hasAssociatedMember(&$Member)
544 $options = $this->getOptions($this->association_id
);
545 if($options['unique'] && !$Member->isNewRecord() && isset($this->asssociated_ids
[$Member->getId()])){
548 $id = $this->_getAssociatedMemberId($Member);
553 function _relateAssociatedWithOwner(&$Associated)
555 if(!$this->Owner
->isNewRecord()){
556 $options = $this->getOptions($this->association_id
);
557 if(strtolower($options['join_class_name']) != strtolower(get_class($this->JoinObject
))){
560 if($Associated->isNewRecord() ?
$Associated->save() : true){
561 if(!$this->_getAssociatedMemberId($Associated)){
562 $this->_setAssociatedMemberId($Associated);
565 $foreign_key = $this->Owner
->getId();
566 $association_foreign_key = $Associated->getId();
567 if($foreign_key != $this->JoinObject
->get($options['foreign_key']) ||
568 $association_foreign_key != $this->JoinObject
->get($options['association_foreign_key'])){
570 $this->JoinObject
=& $this->JoinObject
->create(array($options['foreign_key']=> $foreign_key, $options['association_foreign_key']=> $association_foreign_key));
571 $success = !$this->JoinObject
->isNewRecord();
577 $Associated->hasAndBelongsToMany
->__joined
= true;
584 function &_build($association_id, &$AssociatedObject, $reference_associated = true)
586 if($reference_associated){
587 $this->Owner
->$association_id =& $AssociatedObject;
589 $this->Owner
->$association_id = $AssociatedObject;
591 $this->Owner
->$association_id->_AssociationHandler
=& $this;
592 $this->Owner
->$association_id->_associatedAs
= $this->getType();
593 $this->Owner
->$association_id->_associationId
= $association_id;
594 $this->Owner
->_associations
[$association_id] =& $this->Owner
->$association_id;
595 return $this->Owner
->$association_id;
601 function constructSql()
603 $options = $this->getOptions($this->association_id
);
604 if(empty($options['finder_sql'])){
605 $sqlite = substr($this->Owner
->_db
->databaseType
,0,6) == 'sqlite';
606 $options['finder_sql'] = "SELECT {$options['table_name']}.* FROM {$options['table_name']} ".
607 $this->associationJoin().
608 "WHERE ".$this->Owner
->getTableName().'.'.$this->Owner
->getPrimaryKey()." ".
609 ($sqlite ?
' LIKE ' : ' = ').' '.$this->Owner
->quotedId(); // (HACK FOR SQLITE) Otherwise returns wrong data
610 $options['finder_sql'] .= !empty($options['conditions']) ?
' AND '.$options['conditions'].' ' : '';
611 $options['finder_sql'] .= !empty($options['conditions']) ?
' AND '.$options['conditions'].' ' : '';
613 if(empty($options['counter_sql'])){
614 $options['counter_sql'] = substr_replace($options['finder_sql'],'SELECT COUNT(*)',0,strpos($options['finder_sql'],'*')+
1);
616 $this->setOptions($this->association_id
, $options);
619 function associationJoin()
621 $Associated =& $this->getAssociatedModelInstance();
622 $options = $this->getOptions($this->association_id
);
624 return "LEFT OUTER JOIN {$options['join_table']} ON ".
625 "{$options['join_table']}.{$options['association_foreign_key']} = {$options['table_name']}.".$Associated->getPrimaryKey()." ".
626 "LEFT OUTER JOIN ".$this->Owner
->getTableName()." ON ".
627 "{$options['join_table']}.{$options['foreign_key']} = ".$this->Owner
->getTableName().".".$this->Owner
->getPrimaryKey()." ";
635 $options = $this->getOptions($this->association_id
);
636 if(empty($this->Owner
->{$options['handler_name']}->_loaded
) && !$this->Owner
->isNewRecord()){
637 $this->constructSql();
638 $options = $this->getOptions($this->association_id
);
639 $Associated =& $this->getAssociatedModelInstance();
641 if($this->_hasCachedCounter()){
642 $count = $Associated->getAttribute($this->_getCachedCounterAttributeName());
643 }elseif(!empty($options['counter_sql'])){
644 $count = $Associated->countBySql($options['counter_sql']);
646 $count = (strtoupper(substr($options['finder_sql'],0,6)) != 'SELECT') ?
647 $Associated->count($options['foreign_key'].'='.$this->Owner
->quotedId()) :
648 $Associated->countBySql($options['finder_sql']);
651 $count = count($this->Owner
->{$this->association_id
});
655 $this->Owner
->{$this->association_id
} = array();
656 $this->Owner
->{$options['handler_name']}->_loaded
= true;
663 function &build($attributes = array(), $set_as_new_record = true)
665 $options = $this->getOptions($this->association_id
);
666 Ak
::import($options['class_name']);
667 $record =& new $options['class_name']($attributes);
668 $record->_newRecord
= $set_as_new_record;
669 $this->Owner
->{$this->association_id
}[] =& $record;
670 $this->_setAssociatedMemberId($record);
671 $set_as_new_record ?
$this->_relateAssociatedWithOwner($record) : null;
675 function &create($attributes = array())
677 $record =& $this->build($attributes);
678 if(!$this->Owner
->isNewRecord()){
685 function getAssociatedFinderSqlOptions($association_id, $options = array())
687 $options = $this->getOptions($this->association_id
);
688 $Associated =& $this->getAssociatedModelInstance();
689 $table_name = $Associated->getTableName();
690 $owner_id = $this->Owner
->quotedId();
692 $finder_options = array();
694 foreach ($options as $option=>$value) {
696 $finder_options[$option] = trim($Associated->_addTableAliasesToAssociatedSql('_'.$this->association_id
, $value));
700 $finder_options['joins'] = $this->constructSqlForInclusion();
701 $finder_options['selection'] = '';
703 foreach (array_keys($Associated->getColumns()) as $column_name){
704 $finder_options['selection'] .= '_'.$this->association_id
.'.'.$column_name.' AS _'.$this->association_id
.'_'.$column_name.', ';
707 $finder_options['selection'] = trim($finder_options['selection'], ', ');
710 * @todo Refactorize me. This is too confusing
712 $finder_options['conditions'] =
713 // We add previous conditions
714 (!empty($options['conditions']) ?
715 ' AND '.$Associated->_addTableAliasesToAssociatedSql('_'.$this->association_id
, $options['conditions']).' ' : '');
717 return $finder_options;
720 function constructSqlForInclusion()
722 $Associated =& $this->getAssociatedModelInstance();
723 $options = $this->getOptions($this->association_id
);
726 $options['join_table'].' AS _'.$options['join_class_name'].
728 '__owner.'.$this->Owner
->getPrimaryKey().
730 '_'.$options['join_class_name'].'.'.$options['foreign_key'].
733 $options['table_name'].' AS _'.$this->association_id
.
735 '_'.$this->association_id
.'.'.$Associated->getPrimaryKey().
737 '_'.$options['join_class_name'].'.'.$options['association_foreign_key'].' ';
742 function _hasCachedCounter()
744 $Associated =& $this->getAssociatedModelInstance();
745 return $Associated->isAttributePresent($this->_getCachedCounterAttributeName());
748 function _getCachedCounterAttributeName()
750 return $this->association_id
.'_count';
754 function &getAssociatedModelInstance()
756 static $ModelInstance;
757 if(empty($ModelInstance)){
758 $class_name = $this->getOption($this->association_id
, 'class_name');
759 Ak
::import($class_name);
760 $ModelInstance =& new $class_name();
762 return $ModelInstance;
769 if(!$this->Owner
->isNewRecord()){
770 $this->constructSql();
771 $has_and_belongs_to_many_options = $this->getOptions($this->association_id
);
772 $Associated =& $this->getAssociatedModelInstance();
774 $args = func_get_args();
775 $num_args = func_num_args();
777 if(!empty($args[$num_args-1]) && is_array($args[$num_args-1])){
778 $options_in_args = true;
779 $options = $args[$num_args-1];
781 $options_in_args = false;
785 $options['conditions'] = empty($options['conditions']) ?
@$has_and_belongs_to_many_options['finder_sql'] :
786 (empty($has_and_belongs_to_many_options['finder_sql']) ||
strstr($options['conditions'], $has_and_belongs_to_many_options['finder_sql'])
787 ?
$options['conditions'] : $options['conditions'].' AND '.$has_and_belongs_to_many_options['finder_sql']);
789 $options['order'] = empty($options['order']) ?
@$has_and_belongs_to_many_options['order'] : $options['order'];
791 $options['select_prefix'] = '';
793 if($options_in_args){
794 $args[$num_args-1] = $options;
796 $args = empty($args) ?
array('all') : $args;
797 array_push($args, $options);
800 $result =& Ak
::call_user_func_array(array(&$Associated,'find'), $args);
809 return $this->count() === 0;
814 return $this->count();
819 return $this->deleteAll();
825 function afterCreate(&$object)
827 return $this->_afterCallback($object);
830 function afterUpdate(&$object)
832 return $this->_afterCallback($object);
836 function beforeDestroy(&$object)
840 foreach ((array)$object->_associationIds
as $k => $v){
841 if(isset($object->$k) && is_array($object->$k) && isset($object->$v) && method_exists($object->$v, 'getType') && $object->$v->getType() == 'hasAndBelongsToMany'){
843 $success = $object->$v->deleteAll() ?
$success : false;
850 function _afterCallback(&$object)
852 static $joined_items = array();
855 $object_id = $object->getId();
856 foreach (array_keys($object->hasAndBelongsToMany
->models
) as $association_id){
857 $CollectionHandler =& $object->hasAndBelongsToMany
->models
[$association_id];
858 $options = $CollectionHandler->getOptions($association_id);
860 $class_name = strtolower($CollectionHandler->getOption($association_id, 'class_name'));
861 if(!empty($object->$association_id) && is_array($object->$association_id)){
862 $this->_removeDuplicates($object, $association_id);
863 foreach (array_keys($object->$association_id) as $k){
864 if(!empty($object->{$association_id}[$k]) && strtolower(get_class($object->{$association_id}[$k])) == $class_name){
865 $AssociatedItem =& $object->{$association_id}[$k];
866 // This helps avoiding double realation on first time savings
867 if(!in_array($AssociatedItem->__hasAndBelongsToManyMemberId
, $joined_items)){
868 $joined_items[] = $AssociatedItem->__hasAndBelongsToManyMemberId
;
869 if(empty($AssociatedItem->hasAndBelongsToMany
->__joined
) && $AssociatedItem->isNewRecord()?
$AssociatedItem->save() : true){
870 $AssociatedItem->hasAndBelongsToMany
->__joined
= true;
871 $CollectionHandler->JoinObject
=& $CollectionHandler->JoinObject
->create(array($options['foreign_key'] => $object_id ,$options['association_foreign_key'] => $AssociatedItem->getId()));
874 $success = !$CollectionHandler->JoinObject
->isNewRecord() ?
$success : false;
888 function _removeDuplicates(&$object, $association_id)
890 if(!empty($object->{$association_id})){
891 $CollectionHandler =& $object->hasAndBelongsToMany
->models
[$association_id];
892 $options = $CollectionHandler->getOptions($association_id);
893 if(empty($options['unique'])){
896 if($object->isNewRecord()){
899 if($existing = $CollectionHandler->find()){
900 $ids = $existing[0]->collect($existing,'id','id');
905 $class_name = strtolower($CollectionHandler->getOption($association_id, 'class_name'));
906 foreach (array_keys($object->$association_id) as $k){
907 if(!empty($object->{$association_id}[$k]) && strtolower(get_class($object->{$association_id}[$k])) == $class_name && !$object->{$association_id}[$k]->isNewRecord()){
908 $AssociatedItem =& $object->{$association_id}[$k];
909 if(isset($ids[$AssociatedItem->getId()])){
910 unset($object->{$association_id}[$k]);
913 $ids[$AssociatedItem->getId()] = true;