Fixes #149
[akelos.git] / lib / AkActiveRecord / AkAssociations / AkHasOne.php
blob10dbb4c7bf46ce4b2b7ee0f9edf11ce77011019e
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 of a single associated object.
23 * $association is replaced with the symbol passed as the first argument, so
24 * <tt>hasOne('manager')</tt> would add among others <tt>$this->manager->getAttributes()</tt>.
26 * Example: An Account class declares <tt>hasOne('beneficiary');</tt>, which will add:
27 * * <tt>$Account->beneficiary->load()</tt> (similar to <tt>$Beneficiary->find('first', array('conditions' => "account_id = $id"))</tt>)
28 * * <tt>$Account->beneficiary->assign($Beneficiary);</tt> (similar to <tt>$Beneficiary->account_id = $Account->id; $Beneficiary->save()</tt>)
29 * * <tt>$Account->beneficiary->build();</tt> (similar to <tt>$Beneficiary = new Beneficiary("account_id->", $Account->id)</tt>)
30 * * <tt>$Account->beneficiary->create();</tt> (similar to <tt>$b = new Beneficiary("account_id->", $Account->id); $b->save(); $b</tt>)
32 * The declaration can also include an options array to specialize the behavior of the association.
34 * Options are:
35 * * <tt>class_name</tt> - specify the class name of the association. Use it only if that name can't be inferred
36 * from the association name. So <tt>hasOne('manager')</tt> will by default be linked to the "Manager" class, but
37 * if the real class name is "Person", you'll have to specify it with this option.
38 * * <tt>conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
39 * sql fragment, such as "rank = 5".
40 * * <tt>order</tt> - specify the order from which the associated object will be picked at the top. Specified as
41 * an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
42 * * <tt>dependent</tt> - if set to true, the associated object is destroyed when this object is. It's also destroyed if another
43 * association is assigned.
44 * * <tt>foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
45 * of this class in lower-case and "_id" suffixed. So a "Person" class that makes a hasOne association will use "person_id"
46 * as the default foreign_key.
48 * Option examples:
49 * var $hasOne = array(
50 * 'credit_card' => array('dependent' => true),
51 * 'last_comment' => array('class_name' => "Comment", 'order' => "posted_on"),
52 * 'project_manager' => array('class_name' => "Person", 'conditions' => "role = 'project_manager'")
53 * );
55 class AkHasOne extends AkAssociation
57 var $associated_ids = array();
59 function &addAssociated($association_id, $options = array())
61 $default_options = array(
62 'class_name' => empty($options['class_name']) ? AkInflector::camelize($association_id) : $options['class_name'],
63 'foreign_key' => empty($options['foreign_key']) ? AkInflector::singularize($this->Owner->getTableName()).'_id' : $options['foreign_key'],
64 'remote'=>false,
65 'instantiate'=>false,
66 'conditions'=>false,
67 'include_conditions_when_included'=>true,
68 'order'=>false,
69 'include_order_when_included'=>true,
70 'dependent'=>false,
71 'counter_cache'=>false
74 $options = array_merge($default_options, $options);
76 $options['table_name'] = empty($options['table_name']) ? AkInflector::tableize($options['class_name']) : $options['table_name'];
78 $this->setOptions($association_id, $options);
80 $this->addModel($association_id, new AkAssociatedActiveRecord());
82 $associated =& $this->getModel($association_id);
83 $this->setAssociatedId($association_id, $associated->getId());
85 $associated =& $this->_build($association_id, &$associated, false);
87 $this->_saveLoadedHandler($association_id, $associated);
89 if($options['instantiate']){
90 $associated =& $this->addModel($association_id, new $options['class_name']($options['foreign_key'].' = '.$this->Owner->quotedId()));
93 return $associated;
97 /**
98 * Assigns the associate object, extracts the primary key, sets it as the foreign key, and saves the associate object.
100 function &assign($association_id, &$Associated)
102 if(!$this->Owner->isNewRecord()){
103 $Associated->set($this->Owner->$association_id->getAssociationOption('foreign_key'), $this->Owner->getId());
104 $Associated->save();
107 $this->_build($association_id, &$Associated);
108 $this->Owner->$association_id->_loaded = true;
109 return $Associated;
112 function getAssociatedId($association_id)
114 return isset($this->associated_ids[$association_id]) ? $this->associated_ids[$association_id] : false;
118 function getType()
120 return 'hasOne';
124 function getAssociatedFinderSqlOptions($association_id, $options = array())
126 $default_options = array(
127 'conditions' => $this->Owner->$association_id->getAssociationOption('include_conditions_when_included'),
128 'order' => $this->Owner->$association_id->getAssociationOption('include_order_when_included')
131 if(empty($this->Owner->$association_id->__activeRecordObject)){
132 $this->build($association_id, array(), false);
135 $table_name = $this->Owner->$association_id->getTableName();
136 $options = array_merge($default_options, $options);
138 $finder_options = array();
140 foreach ($options as $option=>$available) {
141 if($available){
142 $value = $this->Owner->$association_id->getAssociationOption($option);
143 empty($value) ? null : ($finder_options[$option] = trim($this->Owner->$association_id->_addTableAliasesToAssociatedSql('_'.$association_id, $value)));
147 $finder_options['joins'] = $this->Owner->$association_id->constructSqlForInclusion();
149 $finder_options['selection'] = '';
150 foreach (array_keys($this->Owner->$association_id->getColumns()) as $column_name){
151 $finder_options['selection'] .= '_'.$association_id.'.'.$column_name.' AS _'.$association_id.'_'.$column_name.', ';
153 $finder_options['selection'] = trim($finder_options['selection'], ', ');
155 return $finder_options;
158 function constructSqlForInclusion($association_id)
160 return ' LEFT OUTER JOIN '.
161 $this->Owner->$association_id->getTableName().' AS _'.$association_id.
162 ' ON '.
163 '__owner.'.$this->Owner->getPrimaryKey().
164 ' = '.
165 '_'.$association_id.'.'.$this->Owner->$association_id->getAssociationOption('foreign_key').' ';
168 function &build($association_id, $attributes = array(), $replace_existing = true)
170 $class_name = $this->Owner->$association_id->getAssociationOption('class_name');
171 $foreign_key = $this->Owner->$association_id->getAssociationOption('foreign_key');
172 Ak::import($class_name);
173 $record =& new $class_name($attributes);
174 if ($replace_existing){
175 $record =& $this->replace($association_id, $record, true);
177 if(!$this->Owner->isNewRecord()){
178 $record->set($foreign_key, $this->Owner->getId());
181 $record =& $this->_build($association_id, &$record);
183 return $record;
188 * Returns a new object of the associated type that has been instantiated with attributes
189 * and linked to this object through a foreign key and that has already been
190 * saved (if it passed the validation)
192 function &create($association_id, $attributes = array(), $replace_existing = true)
194 $this->build($association_id, $attributes, $replace_existing);
195 $this->Owner->$association_id->save();
196 $this->Owner->$association_id->_loaded = true;
197 return $this->Owner->$association_id;
200 function &replace($association_id, &$NewAssociated, $dont_save = false)
202 $Associated =& $this->loadAssociated($association_id);
203 if(!empty($Associated->__activeRecordObject) && !empty($NewAssociated->__activeRecordObject) && $Associated->getId() == $NewAssociated->getId()){
204 return $NewAssociated;
207 if(!empty($Associated->__activeRecordObject)){
208 if ($Associated->getAssociationOption('dependent') && !$dont_save){
209 if(!$Associated->isNewRecord()){
210 $Associated->destroy();
212 }elseif(!$dont_save){
213 $Associated->set($Associated->getAssociationOption('foreign_key'), null);
214 if($Associated->isNewRecord()){
215 $Associated->save();
220 $result = false;
222 if (!empty($NewAssociated->__activeRecordObject)){
223 if(!$this->Owner->isNewRecord()){
224 $NewAssociated->set($Associated->getAssociationOption('foreign_key'), $this->Owner->getId());
227 $NewAssociated =& $this->_build($association_id, &$NewAssociated);
229 $NewAssociated->_loaded = true;
230 if(!$NewAssociated->isNewRecord() || !$dont_save){
231 if($NewAssociated->save()){
232 return $NewAssociated;
234 }else{
235 return $NewAssociated;
238 return $result;
241 function &findAssociated($association_id)
243 if(!$this->Owner->getId()){
244 return false;
246 if(empty($this->Owner->$association_id->__activeRecordObject)){
247 $this->build($association_id, array(), false);
250 $table_name = $this->Owner->$association_id->getAssociationOption('table_name');
252 $finder_options = array(
253 'conditions' => trim($this->Owner->$association_id->_addTableAliasesToAssociatedSql($table_name, $this->constructSqlConditions($association_id))),
254 'selection' => $table_name,
255 'joins' => trim($this->Owner->$association_id->_addTableAliasesToAssociatedSql($table_name, $this->constructSql($association_id))),
256 'order' => trim($this->Owner->$association_id->_addTableAliasesToAssociatedSql($table_name, $this->Owner->$association_id->getAssociationOption('order')))
260 * todo we will use a select statement later
262 $sql = $this->Owner->constructFinderSqlWithAssociations($finder_options, false);//.' LIMIT 1';
263 if($results =& $this->Owner->$association_id->findBySql($sql)){
264 $result =& $results[0];
267 return $result;
271 function constructSqlConditions($association_id)
273 $foreign_key = $this->Owner->$association_id->getAssociationOption('foreign_key');
274 $conditions = $this->Owner->$association_id->getAssociationOption('conditions');
276 $foreign_key_value = $this->Owner->getId();
277 if(empty($foreign_key_value)){
278 return $conditions;
280 return (empty($conditions) ? '' : $conditions.' AND ').$foreign_key.' = '.$this->Owner->castAttributeForDatabase($foreign_key, $foreign_key_value);
283 function constructSql($association_id)
285 $foreign_key = $this->Owner->$association_id->getAssociationOption('foreign_key');
286 $table_name = $this->Owner->$association_id->getAssociationOption('table_name');
287 $owner_table = $this->Owner->getTableName();
289 return ' LEFT OUTER JOIN '.$owner_table.' ON '.$owner_table.'.'.$this->Owner->getPrimaryKey().' = '.$table_name.'.'.$foreign_key;
294 * Triggers
296 function afterSave(&$object)
298 $success = true;
299 $associated_ids = $object->getAssociatedIds();
301 foreach ($associated_ids as $associated_id){
302 if(!empty($object->$associated_id->__activeRecordObject)){
304 if(strtolower($object->hasOne->getOption($associated_id, 'class_name')) == strtolower($object->$associated_id->getType())){
305 $object->hasOne->replace($associated_id, $object->$associated_id, false);
306 $object->$associated_id->set($object->hasOne->getOption($associated_id, 'foreign_key'), $object->getId());
307 $success = $object->$associated_id->save() ? $success : false;
309 }elseif($object->$associated_id->getType() == 'hasOne'){
310 $attributes = array();
311 foreach ((array)$object->$associated_id as $k=>$v){
312 $k[0] != '_' ? $attributes[$k] = $v : null;
314 $attributes = array_diff($attributes, array(''));
315 if(!empty($attributes)){
316 $object->hasOne->build($associated_id, $attributes);
321 return $success;
325 function afterDestroy(&$object)
327 $success = true;
328 $associated_ids = $object->getAssociatedIds();
329 foreach ($associated_ids as $associated_id){
330 if( isset($object->$associated_id->_associatedAs) &&
331 $object->$associated_id->_associatedAs == 'hasOne' &&
332 $object->$associated_id->getAssociationOption('dependent')){
333 if ($object->$associated_id->getType() == 'hasOne'){
334 $object->$associated_id->load();
336 if(method_exists($object->$associated_id, 'destroy')){
337 $success = $object->$associated_id->destroy() ? $success : false;
341 return $success;