Fixes #149
[akelos.git] / lib / AkActiveRecord / AkAssociations / AkHasMany.php
blobcc9643b4e27db475b62388906ab964c951bd2669
1 <?php
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 // +----------------------------------------------------------------------+
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>
18 require_once(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkAssociation.php');
20 /**
21 * Adds the following methods for retrieval and query of collections of associated objects.
22 * collection is replaced with the singular form of current association,
23 * so var $has_many = 'clients' would hold an array of objects on $this->clients
24 * and a collection handling interface instance on $this->client (singular form)
26 * * collection->load($force_reload = false) - returns an array of all the associated objects. An empty array is returned if none are found.
27 * * collection->add($object, ?) - adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
28 * (collection->push and $collection->concat are aliases to this method).
29 * * collection->delete($object, ?) - removes one or more objects from the collection by setting their foreign keys to NULL. This will also destroy the objects if they?re declared as belongs_to and dependent on this model.
30 * * collection->set($objects) - replaces the collections content by deleting and adding objects as appropriate.
31 * * collection->setByIds($ids) - replace the collection by the objects identified by the primary keys in ids
32 * * collection->clear() - removes every object from the collection. This destroys the associated objects if they are 'dependent', deletes them directly from the database if they are 'dependent' => 'delete_all', and sets their foreign keys to NULL otherwise.
33 * * collection->isEmpty() - returns true if there are no associated objects.
34 * * collection->getSize() - returns the number of associated objects.
35 * * collection->find() - finds an associated object according to the same rules as ActiveRecord->find.
36 * * collection->count() - returns the number of elements associated. (collection->size() is an alias to this method)
37 * * collection->build($attributes = array()) - returns a new object of the collection type that has been instantiated with attributes and linked to this object through a foreign key but has not yet been saved. *Note:* This only works if an associated object already exists, not if it?s null
38 * * collection->create($attributes = array()) - returns a new object of the collection type that has been instantiated with attributes and linked to this object through a foreign key and that has already been saved (if it passed the validation). *Note:* This only works if an associated object already exists, not if it?s null
40 * Example: A Firm class declares has_many clients, which will add:
42 * * Firm->client->load() (similar to $Clients->find('all', array('conditions' => 'firm_id = '.$id)) )
43 * * Firm->client->add()
44 * * Firm->client->delete()
45 * * Firm->client->assign()
46 * * Firm->client->assignByIds()
47 * * Firm->client->clear()
48 * * Firm->client->isEmpty() (similar to count($Firm->clients) == 0)
49 * * Firm->client->getSize() (similar to Client.count "firm_id = #{id}")
50 * * Firm->client->find() (similar to $Client->find($id, array('conditions' => 'firm_id = '.$id)) )
51 * * Firm->client->build() (similar to new Client(array('firm_id' => $id)) )
52 * * Firm->client->create() (similar to $c = new Client(array('firm_id' => $id)); $c->save(); return $c )
54 * The declaration can also include an options array to specialize the behavior of the association.
56 * Options are:
58 * * 'class_name' - specify the class name of the association. Use it only if that name can't be inferred from the association name. So "$has_many = 'products'" will by default be linked to the Product class, but if the real class name is SpecialProduct, you?ll have to specify it with this option.
59 * * 'conditions' - specify the conditions that the associated objects must meet in order to be included as a "WHERE" sql fragment, such as "price > 5 AND name LIKE ?B%?".
60 * * 'order' - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment, such as "last_name, first_name DESC"
61 * * 'group' - specify the attribute by which the associated objects are returned as a "GROUP BY" sql fragment, such as "category"
62 * * 'foreign_key' - specify the foreign key used for the association. By default this is guessed to be the name of this class in lower-case and "_id" suffixed. So a Person class that makes a has_many association will use "person_id" as the default foreign_key.
63 * * 'dependent' - if set to 'destroy' all the associated objects are destroyed alongside this object by calling their destroy method. If set to 'delete_all' all associated objects are deleted without calling their destroy method. If set to 'nullify' all associated objects? foreign keys are set to NULL without calling their save callbacks.
64 * * 'finder_sql' - specify a complete SQL statement to fetch the association. This is a good way to go for complex associations that depend on multiple tables. Note: When this option is used, findInCollection is not added.
65 * * 'counter_sql' - specify a complete SQL statement to fetch the size of the association. If +'finder_sql'+ is specified but +'counter_sql'+, +'counter_sql'+ will be generated by replacing SELECT ? FROM with SELECT COUNT(*) FROM.
66 * * 'include' - specify second-order associations that should be eager loaded when the collection is loaded.
67 * * 'group' An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
68 * * 'limit' An integer determining the limit on the number of rows that should be returned.
69 * * 'offset' An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
70 * * 'select' By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not include the joined columns.
72 * Option examples:
74 * $has_many = array(
75 * 'comments' => array('order' => 'posted_on', 'include' => 'author', 'dependent' => 'nullify'),
76 * 'people' => array('conditions' => 'deleted = 0', 'order' => 'name'),
77 * 'tracks' => array('order' => 'position', 'dependent' => 'destroy'),
78 * 'members' => array('class_name' => 'Person', 'conditions' => 'role = "merber"'));
80 class AkHasMany extends AkAssociation
82 var $associated_ids = array();
83 var $association_id;
85 function &addAssociated($association_id, $options = array())
88 $default_options = array(
89 'class_name' => empty($options['class_name']) ? AkInflector::classify($association_id) : $options['class_name'],
90 'conditions' => false,
91 'order' => false,
92 'include_conditions_when_included' => true,
93 'include_order_when_included' => true,
94 'group' => false,
95 'foreign_key' => false,
96 'dependent' => 'nullify',
97 'finder_sql' => false,
98 'counter_sql' => false,
99 'include' => false,
100 'instantiate' => false,
101 'group' => false,
102 'limit' => false,
103 'offset' => false,
104 'handler_name' => strtolower(AkInflector::underscore(AkInflector::singularize($association_id))),
105 'select' => false
108 $options = array_merge($default_options, $options);
110 $options['foreign_key'] = empty($options['foreign_key']) ? AkInflector::underscore($this->Owner->getModelName()).'_id' : $options['foreign_key'];
112 $Collection =& $this->_setCollectionHandler($association_id, $options['handler_name']);
113 $Collection->setOptions($association_id, $options);
116 $this->addModel($association_id, $Collection);
118 if($options['instantiate']){
119 $associated =& $Collection->load();
122 $this->setAssociatedId($association_id, $options['handler_name']);
123 $Collection->association_id = $association_id;
125 return $Collection;
128 function getType()
130 return 'hasMany';
133 function &_setCollectionHandler($association_id, $handler_name)
135 if(isset($this->Owner->$association_id)){
136 if(!is_array($this->Owner->$association_id)){
137 trigger_error(Ak::t('%model_name::%association_id is not a collection array on current %association_id hasMany association',array('%model_name'=>$this->Owner->getModelName(), '%association_id'=>$association_id)), E_USER_NOTICE);
139 $associated =& $this->Owner->$association_id;
140 }else{
141 $associated = array();
142 $this->Owner->$association_id =& $associated;
145 if(isset($this->Owner->$handler_name)){
146 trigger_error(Ak::t('Could not load %association_id on %model_name because "%model_name->%handler_name" attribute '.
147 'is already defided and can\' be used as an association placeholder',
148 array('%model_name'=>$this->Owner->getModelName(),'%association_id'=>$association_id, '%handler_name'=>$handler_name)),
149 E_USER_ERROR);
150 return false;
151 }else{
152 $this->Owner->$handler_name =& new AkHasMany($this->Owner);
154 return $this->Owner->$handler_name;
158 function &load($force_reload = false)
160 $options = $this->getOptions($this->association_id);
161 if($force_reload || empty($this->Owner->{$options['handler_name']}->_loaded)){
162 if(!$this->Owner->isNewRecord()){
163 $this->constructSql(false);
164 $options = $this->getOptions($this->association_id);
165 $Associated =& $this->getAssociatedModelInstance();
166 $finder_options = array('conditions'=>$options['finder_sql']);
167 if(!empty($options['order'])){
168 $finder_options['order'] = $options['order'];
170 if($FoundAssociates = $Associated->find('all',$finder_options)){
171 array_map(array(&$this,'_setAssociatedMemberId'),$FoundAssociates);
172 $this->Owner->{$this->association_id} =& $FoundAssociates;
175 if(empty($this->Owner->{$this->association_id})){
176 $this->Owner->{$this->association_id} = array();
179 $this->Owner->{$options['handler_name']}->_loaded = true;
181 return $this->Owner->{$this->association_id};
186 * add($object), add(array($object, $object2)) - adds one or more objects to the collection by setting
187 * their foreign keys to the collection?s primary key. Items are saved automatically when parent has been saved.
189 function add(&$Associated)
191 if(is_array($Associated)){
192 $succes = true;
193 $succes = $this->Owner->notifyObservers('beforeAdd') ? $succes : false;
194 $options = $this->getOptions($this->association_id);
195 foreach (array_keys($Associated) as $k){
196 if($succes && !empty($options['before_add']) && method_exists($this->Owner, $options['before_add']) && $this->Owner->{$options['before_add']}($Associated[$k]) === false ){
197 $succes = false;
199 if($succes && !$this->_hasAssociatedMember($Associated[$k])){
200 $this->Owner->{$this->association_id}[] =& $Associated[$k];
201 $this->_setAssociatedMemberId($Associated[$k]);
202 if($this->_relateAssociatedWithOwner($Associated[$k])){
203 $succes = $Associated[$k]->save() ? $succes : false;
204 if($succes && !empty($options['after_add']) && method_exists($this->Owner, $options['after_add']) && $this->Owner->{$options['after_add']}($Associated[$k]) === false ){
205 $succes = false;
210 $succes = $this->Owner->notifyObservers('afterAdd') ? $succes : false;
211 return $succes;
212 }else{
213 $associates = array();
214 $associates[] =& $Associated;
215 return $this->add($associates);
219 function push(&$record)
221 return $this->add($record);
224 function concat(&$record)
226 return $this->add($record);
230 * Remove all records from this association
232 function deleteAll($Skip = null)
234 $this->load();
235 return $this->delete($this->Owner->{$this->association_id}, $Skip);
238 function reset()
240 $options = $this->getOptions($this->association_id);
241 $this->Owner->{$options['handler_name']}->_loaded = false;
244 function set(&$objects)
246 $this->deleteAll($objects);
247 $this->add($objects);
250 function setIds()
252 $ids = func_get_args();
253 $ids = is_array($ids[0]) ? $ids[0] : $ids;
255 $AssociatedModel =& $this->getAssociatedModelInstance();
256 if(!empty($ids)){
257 $NewAssociates =& $AssociatedModel->find($ids);
258 $this->set($NewAssociates);
262 function setByIds()
264 $ids = func_get_args();
265 call_user_func_array(array($this,'setIds'), $ids);
268 function addId($id)
270 $AssociatedModel =& $this->getAssociatedModelInstance();
271 if($NewAssociated = $AssociatedModel->find($id)){
272 return $this->add($NewAssociated);
274 return false;
278 function delete(&$Associated, $Skip = null)
280 $success = true;
281 if(!is_array($Associated)){
282 $associated_elements = array();
283 $associated_elements[] =& $Associated;
284 return $this->delete($associated_elements, $Skip);
285 }else{
286 $options = $this->getOptions($this->association_id);
288 $ids_to_skip = array();
289 $Skip = empty($Skip) ? null : (is_array($Skip) ? $Skip : array($Skip));
290 if(!empty($Skip)){
291 foreach (array_keys($Skip) as $k){
292 $ids_to_skip[] = $Skip[$k]->getId();
296 $ids_to_nullify = array();
297 $ids_to_delete = array();
298 $items_to_remove_from_collection = array();
299 $AssociatedModel =& $this->getAssociatedModelInstance();
301 $owner_type = $this->_findOwnerTypeForAssociation($AssociatedModel, $this->Owner);
303 foreach (array_keys($Associated) as $k){
304 $items_to_remove_from_collection[] = $Associated[$k]->getId();
305 if(!in_array($Associated[$k]->getId() , $ids_to_skip)){
306 switch ($options['dependent']) {
307 case 'destroy':
308 $success = $Associated[$k]->destroy() ? $success : false;
309 break;
310 case 'delete_all':
311 $ids_to_delete[] = $Associated[$k]->getId();
312 break;
313 case 'nullify':
314 $id_to_nullify = $Associated[$k]->quotedId();
315 if(!empty($id_to_nullify)){
316 $ids_to_nullify[] = $id_to_nullify;
318 default:
319 break;
324 $ids_to_nullify = empty($ids_to_nullify) ? false : array_diff($ids_to_nullify,array(''));
325 if(!empty($ids_to_nullify)){
326 $success = $AssociatedModel->updateAll(
327 ' '.$options['foreign_key'].' = NULL ',
328 ' '.$options['foreign_key'].' = '.$this->Owner->quotedId().' AND '.$AssociatedModel->getPrimaryKey().' IN ('.join(', ',$ids_to_nullify).')'
329 ) ? $success : false;
330 }elseif(!empty($ids_to_delete)){
331 $success = $AssociatedModel->delete($ids_to_delete) ? $success : false;
334 $this->removeFromCollection($items_to_remove_from_collection);
337 return $success;
343 * Remove records from the collection. Use delete() in order to trigger database dependencies
345 function removeFromCollection(&$records)
347 if(!is_array($records)){
348 $records_array = array();
349 $records_array[] =& $records;
350 $this->delete($records_array);
351 }else{
352 $this->Owner->notifyObservers('beforeRemove');
353 $options = $this->getOptions($this->association_id);
354 foreach (array_keys($records) as $k){
356 if(!empty($options['before_remove']) && method_exists($this->Owner, $options['before_remove']) && $this->Owner->{$options['before_remove']}($records[$k]) === false ){
357 continue;
360 if(isset($records[$k]->__activeRecordObject)){
361 $record_id = $records[$k]->getId();
362 }else{
363 $record_id = $records[$k];
366 foreach (array_keys($this->Owner->{$this->association_id}) as $kk){
369 !empty($this->Owner->{$this->association_id}[$kk]->__hasManyMemberId) &&
370 !empty($records[$k]->__hasManyMemberId) &&
371 $records[$k]->__hasManyMemberId == $this->Owner->{$this->association_id}[$kk]->__hasManyMemberId
372 ) || (
373 !empty($this->Owner->{$this->association_id}[$kk]->__activeRecordObject) &&
374 $record_id == $this->Owner->{$this->association_id}[$kk]->getId()
377 unset($this->Owner->{$this->association_id}[$kk]);
381 $this->_unsetAssociatedMemberId($records[$k]);
383 if(!empty($options['after_remove']) && method_exists($this->Owner, $options['after_remove'])){
384 $this->Owner->{$options['after_remove']}($records[$k]);
388 $this->Owner->notifyObservers('afterRemove');
395 function _setAssociatedMemberId(&$Member)
397 if(empty($Member->__hasManyMemberId)) {
398 $Member->__hasManyMemberId = Ak::randomString();
400 $object_id = method_exists($Member,'getId') ? $Member->getId() : null;
401 if(!empty($object_id)){
402 $this->associated_ids[$object_id] = $Member->__hasManyMemberId;
406 function _unsetAssociatedMemberId(&$Member)
408 $id = $this->_getAssociatedMemberId($Member);
409 unset($this->associated_ids[$id]);
410 unset($Member->__hasManyMemberId);
413 function _getAssociatedMemberId(&$Member)
415 if(!empty($Member->__hasManyMemberId)) {
416 return array_search($Member->__hasManyMemberId, $this->associated_ids);
418 return false;
421 function _hasAssociatedMember(&$Member)
423 return !empty($Member->__hasManyMemberId);
427 function _relateAssociatedWithOwner(&$Associated)
429 if(!$this->Owner->isNewRecord()){
430 if(method_exists($Associated, 'getModelName')){
431 $foreign_key = $this->getOption($this->association_id, 'foreign_key');
432 if($this->getOption($this->association_id, 'class_name') != $Associated->getModelName() || $foreign_key == $Associated->get($foreign_key)){
433 return false;
435 $Associated->set($foreign_key, $this->Owner->getId());
436 return true;
439 return false;
442 function &_build($association_id, &$AssociatedObject, $reference_associated = true)
444 if($reference_associated){
445 $this->Owner->$association_id =& $AssociatedObject;
446 }else{
447 $this->Owner->$association_id = $AssociatedObject;
449 $this->Owner->$association_id->_AssociationHandler =& $this;
450 $this->Owner->$association_id->_associatedAs = $this->getType();
451 $this->Owner->$association_id->_associationId = $association_id;
452 $this->Owner->_associations[$association_id] =& $this->Owner->$association_id;
453 return $this->Owner->$association_id;
459 function constructSql($set_owner_table_has_included = true)
461 $options = $this->getOptions($this->association_id);
462 $Associated =& $this->getAssociatedModelInstance();
463 $owner_id = $this->Owner->quotedId();
464 $table_name = (!empty($options['include']) || $set_owner_table_has_included) ? '__owner' : $Associated->getTableName();
466 if(empty($options['finder_sql'])){
467 $options['finder_sql'] = ' '.$table_name.'.'.$options['foreign_key'].' = '.(empty($owner_id) ? 'null' : $owner_id).' ';
468 $options['finder_sql'] .= !empty($options['conditions']) ? ' AND '.$options['conditions'].' ' : '';
470 if(empty($options['counter_sql']) && !empty($options['finder_sql'])){
471 $options['counter_sql'] = $options['finder_sql'];
472 }elseif(empty($options['counter_sql'])){
473 $options['counter_sql'] = ' '.$table_name.'.'.$options['foreign_key'].' = '.(empty($owner_id) ? 'null' : $owner_id).' ';
474 $options['counter_sql'] .= !empty($options['conditions']) ? ' AND '.$options['conditions'].' ' : '';
477 if(!empty($options['counter_sql']) && strtoupper(substr($options['counter_sql'],0,6)) != 'SELECT'){
478 $options['counter_sql'] = 'SELECT COUNT(*) FROM '.$table_name.' WHERE '.$options['counter_sql'];
481 $this->setOptions($this->association_id, $options);
486 function count($force_count = false)
488 $count = 0;
489 $options = $this->getOptions($this->association_id);
490 if($force_count || (empty($this->Owner->{$options['handler_name']}->_loaded) && !$this->Owner->isNewRecord())){
491 $this->constructSql(false);
492 $options = $this->getOptions($this->association_id);
493 $Associated =& $this->getAssociatedModelInstance();
495 if($this->_hasCachedCounter()){
496 $count = $Associated->getAttribute($this->_getCachedCounterAttributeName());
497 }elseif(!empty($options['counter_sql'])){
498 $count = $Associated->countBySql($options['counter_sql']);
499 }else{
500 $count = (strtoupper(substr($options['finder_sql'],0,6)) != 'SELECT') ?
501 $Associated->count($options['foreign_key'].'='.$this->Owner->quotedId()) :
502 $Associated->countBySql($options['finder_sql']);
504 }else{
505 $count = count($this->Owner->{$this->association_id});
508 if($count == 0){
509 $this->Owner->{$this->association_id} = array();
510 $this->Owner->{$options['handler_name']}->_loaded = true;
513 return $count;
516 function size()
518 return $this->count();
522 function &build($attributes = array(), $set_as_new_record = true)
524 $options = $this->getOptions($this->association_id);
525 Ak::import($options['class_name']);
526 $record =& new $options['class_name']($attributes);
527 $record->_newRecord = $set_as_new_record;
528 $this->Owner->{$this->association_id}[] =& $record;
529 $this->_setAssociatedMemberId($record);
530 $this->_relateAssociatedWithOwner($record);
531 return $record;
534 function &create($attributes = array())
536 $record =& $this->build($attributes);
537 if(!$this->Owner->isNewRecord()){
538 $record->save();
540 return $record;
544 function getAssociatedFinderSqlOptions($association_id, $options = array())
546 $options = $this->getOptions($this->association_id);
547 $Associated =& $this->getAssociatedModelInstance();
548 $table_name = $Associated->getTableName();
549 $owner_id = $this->Owner->quotedId();
551 $finder_options = array();
553 foreach ($options as $option=>$value) {
554 if(!empty($value)){
555 $finder_options[$option] = trim($Associated->_addTableAliasesToAssociatedSql('_'.$this->association_id, $value));
559 $finder_options['joins'] = $this->constructSqlForInclusion();
560 $finder_options['selection'] = '';
562 foreach (array_keys($Associated->getColumns()) as $column_name){
563 $finder_options['selection'] .= '_'.$this->association_id.'.'.$column_name.' AS _'.$this->association_id.'_'.$column_name.', ';
566 $finder_options['selection'] = trim($finder_options['selection'], ', ');
568 $finder_options['conditions'] = empty($options['conditions']) ? '' :
570 $Associated->_addTableAliasesToAssociatedSql('_'.$this->association_id, $options['conditions']).' ';
572 return $finder_options;
575 function constructSqlForInclusion()
577 $Associated =& $this->getAssociatedModelInstance();
578 $options = $this->getOptions($this->association_id);
579 return ' LEFT OUTER JOIN '.
580 $Associated->getTableName().' AS _'.$this->association_id.
581 ' ON '.
582 '__owner.'.$this->Owner->getPrimaryKey().
583 ' = '.
584 '_'.$this->association_id.'.'.$options['foreign_key'].' ';
588 function _hasCachedCounter()
590 $Associated =& $this->getAssociatedModelInstance();
591 return $Associated->isAttributePresent($this->_getCachedCounterAttributeName());
594 function _getCachedCounterAttributeName()
596 return $this->association_id.'_count';
600 function &getAssociatedModelInstance()
602 static $ModelInstances;
603 $class_name = $this->getOption($this->association_id, 'class_name');
604 if(empty($ModelInstances[$class_name])){
605 Ak::import($class_name);
606 $ModelInstances[$class_name] =& new $class_name();
608 return $ModelInstances[$class_name];
612 function &find()
614 $result = false;
615 if(!$this->Owner->isNewRecord()){
617 $args = func_get_args();
618 $num_args = func_num_args();
620 if(!empty($args[$num_args-1]) && is_array($args[$num_args-1])){
621 $options_in_args = true;
622 $options = $args[$num_args-1];
623 }else{
624 $options_in_args = false;
625 $options = array();
628 $this->constructSql(!empty($options['include']));
629 $has_many_options = $this->getOptions($this->association_id);
630 $Associated =& $this->getAssociatedModelInstance();
631 if (empty($options['conditions'])) {
632 $options['conditions'] = @$has_many_options['finder_sql'];
633 } elseif(!empty($has_many_options['finder_sql']) && is_array($options['conditions']) && !strstr($options['conditions'][0], $has_many_options['finder_sql'])) {
634 $options['conditions'][0] .= ' AND '. $has_many_options['finder_sql'];
635 } elseif (!empty($has_many_options['finder_sql']) && !strstr($options['conditions'], $has_many_options['finder_sql'])) {
636 $options['conditions'] .= ' AND '. $has_many_options['finder_sql'];
639 $options['order'] = empty($options['order']) ? @$has_many_options['order'] : $options['order'];
640 $options['include'] = empty($options['include']) ? @$has_many_options['include'] : $options['include'];
642 if($options_in_args){
643 $args[$num_args-1] = $options;
644 }else{
645 $args = empty($args) ? array('all') : $args;
646 array_push($args, $options);
649 $result =& Ak::call_user_func_array(array(&$Associated,'find'), $args);
652 return $result;
656 function isEmpty()
658 return $this->count() === 0;
661 function getSize()
663 return $this->count();
666 function clear()
668 return $this->deleteAll();
672 * Triggers
674 function afterCreate(&$object)
676 return $this->_afterCallback($object);
679 function afterUpdate(&$object)
681 return $this->_afterCallback($object);
685 function beforeDestroy(&$object)
687 $success = true;
689 foreach ((array)$object->_associationIds as $k => $v){
690 if(isset($object->$k) && is_array($object->$k) && isset($object->$v) && method_exists($object->$v, 'getType') && $object->$v->getType() == 'hasMany'){
692 $ids_to_delete = array();
693 $ids_to_nullify = array();
694 $items_to_remove_from_collection = array();
696 $object->$v->load();
697 foreach(array_keys($object->$k) as $key){
699 $items_to_remove_from_collection[] =& $object->{$k}[$key];
701 switch ($object->$v->options[$k]['dependent']) {
703 case 'destroy':
704 $success = $object->{$k}[$key]->destroy() ? $success : false;
705 break;
707 case 'delete_all':
708 $ids_to_delete[] = $object->{$k}[$key]->getId();
709 break;
711 case 'nullify':
712 $id_to_nullify = $object->{$k}[$key]->quotedId();
713 if(!empty($id_to_nullify)){
714 $ids_to_nullify[] = $id_to_nullify;
716 break;
718 default:
719 break;
723 $ids_to_nullify = empty($ids_to_nullify) ? false : array_diff($ids_to_nullify,array(''));
724 if(!empty($ids_to_nullify)){
725 $success = $object->{$k}[$key]->updateAll(
726 ' '.$object->$v->options[$k]['foreign_key'].' = NULL ',
727 ' '.$object->$v->options[$k]['foreign_key'].' = '.$object->quotedId().' AND '.$object->{$k}[$key]->getPrimaryKey().' IN ('.join(', ', $ids_to_nullify).')'
728 ) ? $success : false;
729 }elseif(!empty($ids_to_delete)){
730 $success = $object->{$k}[$key]->delete($ids_to_delete) ? $success : false;
732 $object->$v->removeFromCollection($items_to_remove_from_collection);
736 return $success;
739 function _afterCallback(&$object)
742 $success = true;
744 $object_id = $object->getId();
745 foreach (array_keys($object->hasMany->models) as $association_id){
746 $CollectionHandler =& $object->hasMany->models[$association_id];
747 $foreign_key = $CollectionHandler->getOption($association_id, 'foreign_key');
748 $class_name = strtolower($CollectionHandler->getOption($association_id, 'class_name'));
749 if(!empty($object->$association_id) && is_array($object->$association_id)){
750 foreach (array_keys($object->$association_id) as $k){
751 if(!empty($object->{$association_id}[$k]) && strtolower(get_class($object->{$association_id}[$k])) == strtolower($class_name)){
752 $AssociatedItem =& $object->{$association_id}[$k];
753 $AssociatedItem->set($foreign_key, $object_id);
754 $success = !$AssociatedItem->save() ? false : $success;
759 return $success;