Fixes #149
[akelos.git] / lib / AkActiveRecord / AkAssociations / AkBelongsTo.php
blobd0e7845ceb859df48f973b21938ff0fce5d835bf
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>
19 require_once(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkAssociation.php');
21 /**
22 * Adds the following methods for retrieval and query for a single associated object that this object holds an id to.
23 * * <tt>belongsTo->assign($association_id, $Associate);</tt> - assigns the associate object, extracts the primary key, and sets it as the foreign key.
24 * * <tt>belongsTo->build($association_id, $attributes = array())</tt> - returns a new object of the associated type that has been instantiated
25 * with +attributes+ and linked to this object through a foreign key but has not yet been saved.
26 * * <tt>belongsTo->create($association_id, $attributes = array())</tt> - returns a new object of the associated type that has been instantiated
27 * with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
29 * Example: A Post class declares <tt>belongsTo('author')</tt>, which will add:
30 * * <tt>$Post->author->load()</tt> (similar to <tt>$Author->find($author_id)</tt>)
31 * * <tt>$Post->author->assign($Author)</tt> (similar to <tt>$Post->author_id = $Author->getId();</tt>)
32 * * <tt>$Post->author->build($Author);</tt> (similar to <tt>$Post->author = new Author();</tt>)
33 * * <tt>$Post->author->create($Author);</tt> (similar to <tt>$Post->author = new Author(); $Post->author->save();</tt>)
34 * The declaration can also include an options hash to specialize the behavior of the association.
36 * Options are:
37 * * <tt>class_name</tt> - specify the class name of the association. Use it only if that name can't be inferred
38 * from the association name. So <tt>belongsTo('author')</tt> will by default be linked to the 'Author' class, but
39 * if the real class name is 'Person', you'll have to specify it with this option.
40 * * <tt>conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
41 * sql fragment, such as "authorized = 1".
42 * * <tt>order</tt> - specify the order from which the associated object will be picked at the top. Specified as
43 * an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
44 * * <tt>primary_key_name</tt> - specify the foreign key used for the association. By default this is guessed to be the name
45 * of the associated class in lower-case and "_id" suffixed. So a 'Person' class that makes a belongsTo association to a
46 * 'Boss' class will use "boss_id" as the default primary_key_name.
47 * * <tt>counter_cache</tt> - caches the number of belonging objects on the associate class through use of increment_counter
48 * and decrement_counter. The counter cache is incremented when an object of this class is created and decremented when it's
49 * destroyed. This requires that a column named "#{table_name}_count" (such as comments_count for a belonging Comment class)
50 * is used on the associate class (such as a Post class).
52 * Option examples:
53 * belongsTo('firm', array('primary_key_name' => 'client_of'));
54 * belongsTo('author', array('class_name' => 'Person', 'primary_key_name' => 'author_id'));
55 * belongsTo('valid_coupon', array('class_name' => 'Coupon', 'primary_key_name' => 'coupon_id', 'conditions' => "'discounts' > 'payments_count'"));
57 class AkBelongsTo extends AkAssociation
59 var $associated_ids = array();
61 function &addAssociated($association_id, $options = array())
64 $default_options = array(
65 'class_name' => empty($options['class_name']) ? AkInflector::camelize($association_id) : $options['class_name'],
66 'primary_key_name',
67 'remote',
68 'conditions',
69 'order',
70 //'dependent',
71 'instantiate'=>false,
72 'counter_cache' => false
75 $options = array_merge($default_options, $options);
77 //$options['table_name'] = empty($options['table_name']) ? AkInflector::tableize($options['class_name']) : $options['table_name'];
78 $options['primary_key_name'] = empty($options['primary_key_name']) ? AkInflector::underscore($options['class_name']).'_id' : $options['primary_key_name'];
79 if($options['counter_cache']){
80 $options['counter_cache_column'] = !isset($options['counter_cache_column']) ? AkInflector::underscore($options['class_name']).'_counter' : $options['counter_cache_column'];
83 $this->setOptions($association_id, $options);
85 $associated = $this->addModel($association_id, new AkAssociatedActiveRecord());
87 $this->setAssociatedId($association_id, $associated->getId());
89 $this->_build($association_id, $associated);
91 $this->_saveLoadedHandler($association_id, $associated);
93 if($options['instantiate']){
94 $associated =& $this->assign($association_id, new $options['class_name']($this->Owner->get($options['primary_key_name'])));
97 return $associated;
101 function getType()
103 return 'belongsTo';
107 function &findAssociated($association_id)
109 $result = false;
110 $primary_key_name = $this->Owner->$association_id->getAssociationOption('primary_key_name');
111 $primary_key_name_value = $this->Owner->get($primary_key_name);
112 if(!$primary_key_name_value){
113 return $result;
115 if(empty($this->Owner->$association_id->__activeRecordObject)){
116 $this->build($association_id, array(), false);
119 $result =& $this->Owner->$association_id->find($primary_key_name_value);
121 return $result;
124 function &assign($association_id, &$Associated)
126 $primary_key_name = $this->Owner->$association_id->getAssociationOption('primary_key_name');
127 if($Associated->save()){
128 $this->Owner->set($primary_key_name, $Associated->getId());
130 $Associated =& $this->_build($association_id, &$Associated);
131 return $Associated;
134 function &build($association_id, $attributes = array(), $replace = true)
136 $class_name = $this->Owner->$association_id->getAssociationOption('class_name');
137 Ak::import($class_name);
138 $record =& new $class_name($attributes);
139 $record =& $this->Owner->$association_id->replace($record);
140 return $record;
144 * Returns a new object of the associated type that has been instantiated with attributes
145 * and linked to this object through a foreign key and that has already been saved (if it passed the validation)
147 function &create($association_id, $attributes = array())
149 $class_name = $this->Owner->$association_id->getAssociationOption('class_name');
150 $record =& new $class_name($attributes);
151 $record->save();
152 $this->replace($association_id, $record, true);
153 return $this->Owner->$association_id;
157 function &load($association_id)
159 if (!$this->Owner->isNewRecord()){
160 if(empty($this->Owner->$association_id->_loaded)){
161 if($Associated =& $this->findAssociated($association_id)){
162 $Associated->_loaded = true;
163 $this->_build($association_id, $Associated, false);
167 return $this->Owner->$association_id;
170 function &replace($association_id, &$NewAssociated)
172 $counter_cache_name = $this->Owner->belongsTo->getOption($association_id, 'counter_cache_column');
173 if(empty($NewAssociated)){
174 $primary_key = $this->Owner->belongsTo->getOption($association_id, 'primary_key_name');
175 if($counter_cache_name && isset($this->Owner->$association_id->$counter_cache_name) && !$this->Owner->isNewRecord()){
176 $this->Owner->$association_id->decrementCounter($counter_cache_name, $this->Owner->get($primary_key));
178 $this->Owner->$association_id =& $this->_getLoadedHandler($association_id);
179 $this->Owner->set($primary_key, null);
180 }else{
181 $primary_key = $this->Owner->belongsTo->getOption($association_id, 'primary_key_name');
182 if($counter_cache_name && !$this->Owner->isNewRecord()){
183 $this->Owner->$association_id->incrementCounter($counter_cache_name, $NewAssociated->getId());
184 $previous_id = $this->Owner->get($primary_key);
185 if($previous_id){
186 $this->Owner->$association_id->decrementCounter($counter_cache_name, $previous_id);
189 if(!$NewAssociated->isNewRecord()){
190 $this->Owner->set($primary_key, $NewAssociated->getId());
192 $this->updated[$association_id] = true;
194 $this->updated[$association_id] = true;
195 $this->loaded[$association_id] = true;
197 $this->_build($association_id, $NewAssociated);
198 return $NewAssociated;
202 function getAssociatedFinderSqlOptions($association_id, $options = array())
204 $default_options = array(
205 'conditions' => $this->Owner->$association_id->getAssociationOption('include_conditions_when_included'),
206 'order' => $this->Owner->$association_id->getAssociationOption('include_order_when_included')
209 if(empty($this->Owner->$association_id->__activeRecordObject)){
210 $this->build($association_id, array(), false);
213 $table_name = $this->Owner->$association_id->getTableName();
214 $options = array_merge($default_options, $options);
216 $finder_options = array();
218 foreach ($options as $option=>$available) {
219 if($available){
220 $value = $this->Owner->$association_id->getAssociationOption($option);
221 empty($value) ? null : ($finder_options[$option] = trim($this->Owner->$association_id->_addTableAliasesToAssociatedSql('_'.$association_id, $value)));
225 $finder_options['joins'] = $this->Owner->$association_id->constructSqlForInclusion();
227 $finder_options['selection'] = '';
228 foreach (array_keys($this->Owner->$association_id->getColumns()) as $column_name){
229 $finder_options['selection'] .= '_'.$association_id.'.'.$column_name.' AS _'.$association_id.'_'.$column_name.', ';
231 $finder_options['selection'] = trim($finder_options['selection'], ', ');
233 return $finder_options;
236 function constructSqlForInclusion($association_id)
238 return ' LEFT OUTER JOIN '.
239 $this->Owner->$association_id->getTableName().' AS _'.$association_id.
240 ' ON '.
241 '__owner.'.$this->Owner->$association_id->getAssociationOption('primary_key_name').
242 ' = '.
243 '_'.$association_id.'.'.$this->Owner->$association_id->getPrimaryKey().' ';
248 * Triggers
251 function beforeSave(&$object)
253 $association_ids = $object->getAssociatedIds();
254 foreach ($association_ids as $association_id){
255 if( !empty($object->$association_id->__activeRecordObject) &&
256 strtolower($object->belongsTo->getOption($association_id, 'class_name')) == strtolower($object->$association_id->getType())){
257 $primary_key_name = $this->Owner->belongsTo->getOption($association_id, 'primary_key_name');
258 if($object->$association_id->isNewRecord() && !$object->$association_id->hasAttributesDefined()){
259 $object->$association_id->save(true);
261 $primary_key_name_value = $object->$association_id->getId();
262 if(!empty($primary_key_name_value)){
263 $object->set($primary_key_name, $primary_key_name_value);
267 return true;
270 function beforeDestroy(&$object)
272 $association_ids = $object->getAssociatedIds();
273 foreach ($association_ids as $association_id){
274 if(!empty($object->$association_id) && is_object($object->$association_id) && method_exists($object->$association_id,'getType') &&
275 strtolower($object->belongsTo->getOption($association_id, 'class_name')) == strtolower($object->$association_id->getType())){
276 $primary_key_name = $this->Owner->$association_id->getAssociationOption('primary_key_name');
277 if($this->Owner->$association_id->getAssociationOption('counter_cache')){
278 $object->$association_id->decrementCounter(AkInflector::pluralize($association_id).'_count', $object->get($primary_key_name));
282 return true;