Ensuring tests pass after bringing back support for db DSN connection. Rel. [501...
[akelos.git] / lib / AkActiveRecord / AkAssociatedActiveRecord.php
blobea2b916714cb76c16aeed581e493324cdce34f4c
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 Base
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.'AkBaseModel.php');
21 /**
22 Adds the following methods for retrieval and query of a single associated object. association is replaced with the symbol passed as the first argument, so has_one :manager would add among others manager.nil?.
24 * association(force_reload = false) - returns the associated object. Nil is returned if none is found.
25 * association=(associate) - assigns the associate object, extracts the primary key, sets it as the foreign key, and saves the associate object.
26 * association.nil? - returns true if there is no associated object.
27 * build_association(attributes = {}) - returns a new object of the associated 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 association already exists. It will NOT work if the association is nil.
28 * create_association(attributes = {}) - returns a new object of the associated 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).
30 Example: An Account class declares has_one :beneficiary, which will add:
32 * Account#beneficiary (similar to Beneficiary.find(:first, :conditions => "account_id = #{id}"))
33 * Account#beneficiary=(beneficiary) (similar to beneficiary.account_id = account.id; beneficiary.save)
34 * Account#beneficiary.nil?
35 * Account#build_beneficiary (similar to Beneficiary.new("account_id" => id))
36 * Account#create_beneficiary (similar to b = Beneficiary.new("account_id" => id); b.save; b)
40 class AkAssociatedActiveRecord extends AkBaseModel
42 var $__activeRecordObject = false;
43 var $_AssociationHandler;
44 var $_associationId = false;
45 // Holds different association IDs related to this model
46 var $_associationIds = array();
47 var $_associations = array();
49 function _loadAssociationHandler($association_type)
51 if(empty($this->$association_type) && in_array($association_type, array('hasOne','belongsTo','hasMany','hasAndBelongsToMany'))){
52 $association_handler_class_name = 'Ak'.ucfirst($association_type);
53 require_once(AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkAssociations'.DS.$association_handler_class_name.'.php');
54 $this->$association_type =& new $association_handler_class_name($this);
56 return !empty($this->$association_type);
59 function setAssociationHandler(&$AssociationHandler, $association_id)
61 $this->_AssociationHandler =& $AssociationHandler;
64 function loadAssociations()
66 $association_aliases = array(
67 'hasOne' => array('hasOne','has_one'),
68 'belongsTo' => array('belongsTo','belongs_to'),
69 'hasMany' => array('hasMany','has_many'),
70 'hasAndBelongsToMany' => array('hasAndBelongsToMany', 'habtm', 'has_and_belongs_to_many'),
73 foreach ($association_aliases as $association_type=>$aliases){
74 $association_details = false;
75 foreach ($aliases as $alias){
76 if(empty($association_details) && !empty($this->$alias)){
77 $association_details = $this->$alias;
79 unset($this->$alias);
81 if(!empty($association_details) && $this->_loadAssociationHandler($association_type)){
82 $this->$association_type->initializeAssociated($association_details);
83 $this->_associations[$association_type] =& $this->$association_type;
88 /**
89 * Gets an array of associated object of selected association type.
91 function &getAssociated($association_type)
93 $result = array();
94 if(!empty($this->$association_type) && in_array($association_type, array('hasOne','belongsTo','hasMany','hasAndBelongsToMany'))){
95 $result =& $this->$association_type->getModels();
97 return $result;
100 function getId()
102 return false;
106 function &assign(&$Associated)
108 $result = false;
109 if(is_object($this->_AssociationHandler)){
110 $result =& $this->_AssociationHandler->assign($this->getAssociationId(), $Associated);
112 return $result;
116 * Returns a new object of the associated type that has been instantiated with attributes
117 * and linked to this object through a foreign key but has not yet been saved.
119 function &build($attributes = array(), $replace_existing = true)
121 $result = false;
122 if(!empty($this->_AssociationHandler)){
123 $result =& $this->_AssociationHandler->build($this->getAssociationId(), $attributes, $replace_existing);
125 return $result;
129 function &create($attributes = array(), $replace_existing = true)
131 $result = false;
132 if(!empty($this->_AssociationHandler)){
133 $result =& $this->_AssociationHandler->create($this->getAssociationId(), $attributes, $replace_existing);
135 return $result;
138 function &replace(&$NewAssociated, $dont_save = false)
140 $result = false;
141 if(!empty($this->_AssociationHandler)){
142 $result =& $this->_AssociationHandler->replace($this->getAssociationId(), $NewAssociated, $dont_save = false);
144 return $result;
147 function &find()
149 $result = false;
150 if(!empty($this->_AssociationHandler)){
151 $result =& $this->_AssociationHandler->findAssociated($this->getAssociationId());
153 return $result;
156 function &load()
158 $result = false;
159 if(!empty($this->_AssociationHandler)){
160 $result =& $this->_AssociationHandler->loadAssociated($this->getAssociationId());
162 return $result;
165 function constructSql()
167 return !empty($this->_AssociationHandler) ? $this->_AssociationHandler->constructSql($this->getAssociationId()) : false;
170 function constructSqlForInclusion()
172 return !empty($this->_AssociationHandler) ? $this->_AssociationHandler->constructSqlForInclusion($this->getAssociationId()) : false;
175 function getAssociatedFinderSqlOptions($options = array())
177 return !empty($this->_AssociationHandler) ? $this->_AssociationHandler->getAssociatedFinderSqlOptions($this->getAssociationId(), $options) : false;
180 function getAssociationOption($option)
182 return !empty($this->_AssociationHandler) ? $this->_AssociationHandler->getOption($this->getAssociationId(), $option) : false;
185 function setAssociationOption($option, $value)
187 return !empty($this->_AssociationHandler) ? $this->_AssociationHandler->setOption($this->getAssociationId(), $option, $value) : false;
190 function getAssociationId()
192 if(empty($this->_associationId)){
193 trigger_error(Ak::t('You are trying to access a non associated Object property. '.
194 'This error might have been caused by asigning directly an object '.
195 'to the association instead of using the "assign()" method'),E_USER_WARNING);
197 return $this->_associationId;
200 function getAssociatedIds()
202 return array_keys($this->_associationIds);
205 function getAssociatedHandlerName($association_id)
207 return empty($this->_associationIds[$association_id]) ? false : $this->_associationIds[$association_id];
210 function getAssociatedType()
212 return !empty($this->_AssociationHandler) ? $this->_AssociationHandler->getType() : false;
215 function getAssociationType()
217 return $this->getAssociatedType();
220 function getType()
222 return $this->getAssociatedType();
226 function hasAssociations()
228 return !empty($this->_associations) && count($this->_associations) > 0;
231 function &findWithAssociations($options)
233 $result = false;
234 $options['include'] = is_array($options['include']) ? $options['include'] : array($options['include']);
235 $options['order'] = empty($options['order']) ? '' : $this->_addTableAliasesToAssociatedSql('__owner', $options['order']);
236 $options['conditions'] = empty($options['conditions']) ? '' : $this->_addTableAliasesToAssociatedSql('__owner', $options['conditions']);
238 $included_associations = array();
239 $included_association_options = array();
240 foreach ($options['include'] as $k=>$v){
241 if(is_numeric($k)){
242 $included_associations[] = $v;
243 }else {
244 $included_associations[] = $k;
245 $included_association_options[$k] = $v;
249 $available_associated_options = array('order'=>array(), 'conditions'=>array(), 'joins'=>array(), 'selection'=>array());
251 foreach ($included_associations as $association_id){
252 $association_options = empty($included_association_options[$association_id]) ? array() : $included_association_options[$association_id];
254 $handler_name = $this->getCollectionHandlerName($association_id);
255 $handler_name = empty($handler_name) ? $association_id : (in_array($handler_name, $included_associations) ? $association_id : $handler_name);
256 $associated_options = $this->$handler_name->getAssociatedFinderSqlOptions($association_options);
257 foreach (array_keys($available_associated_options) as $associated_option){
258 if(!empty($associated_options[$associated_option])){
259 $available_associated_options[$associated_option][] = $associated_options[$associated_option];
264 foreach ($available_associated_options as $option=>$values){
265 if(!empty($values)){
266 $separator = $option == 'joins' ? ' ' : (in_array($option, array('selection','order')) ? ', ': ' AND ');
267 $values = array_map('trim', $values);
268 $options[$option] = empty($options[$option]) ?
269 join($separator, $values) :
270 trim($options[$option]).$separator.join($separator, $values);
274 $sql = trim($this->constructFinderSqlWithAssociations($options));
276 if(!empty($options['bind']) && is_array($options['bind']) && strstr($sql,'?')){
277 $sql = array_merge(array($sql),$options['bind']);
279 $result =& $this->_findBySqlWithAssociations($sql, $options['include'], empty($options['virtual_limit']) ? false : $options['virtual_limit']);
281 return $result;
285 function getCollectionHandlerName($association_id)
287 if(isset($this->$association_id) && is_object($this->$association_id) && method_exists($this->$association_id,'getAssociatedFinderSqlOptions')){
288 return false;
290 $collection_handler_name = AkInflector::singularize($association_id);
291 if(isset($this->$collection_handler_name) &&
292 is_object($this->$collection_handler_name) &&
293 in_array($this->$collection_handler_name->getType(),array('hasMany','hasAndBelongsToMany'))){
294 return $collection_handler_name;
295 }else{
296 return false;
302 * Used for generating custom selections for habtm, has_many and has_one queries
304 function constructFinderSqlWithAssociations($options, $include_owner_as_selection = true)
306 $sql = 'SELECT ';
307 $selection = '';
308 if($include_owner_as_selection){
309 foreach (array_keys($this->getColumns()) as $column_name){
310 $selection .= '__owner.'.$column_name.' AS __owner_'.$column_name.', ';
312 $selection .= (isset($options['selection']) ? $options['selection'].' ' : '');
313 $selection = trim($selection,', ').' '; // never used by the unit tests
314 }else{
315 // used only by HasOne::findAssociated
316 $selection .= $options['selection'].'.* ';
318 $sql .= $selection;
319 $sql .= 'FROM '.($include_owner_as_selection ? $this->getTableName().' AS __owner ' : $options['selection'].' ');
320 $sql .= (!empty($options['joins']) ? $options['joins'].' ' : '');
322 empty($options['conditions']) ? null : $this->addConditions($sql, $options['conditions']);
324 // Create an alias for order
325 if(empty($options['order']) && !empty($options['sort'])){
326 $options['order'] = $options['sort'];
328 $sql .= !empty($options['order']) ? ' ORDER BY '.$options['order'] : '';
330 $this->_db->addLimitAndOffset($sql,$options);
331 return $sql;
336 * @todo Refactor in order to increase performance of associated inclussions
338 function &_findBySqlWithAssociations($sql, $included_associations = array(), $virtual_limit = false)
340 $objects = array();
341 $results = $this->_db->execute ($sql,'find with associations');
342 if (!$results){
343 return $objects;
346 $i = 0;
347 $associated_ids = $this->getAssociatedIds();
348 $number_of_associates = count($associated_ids);
349 $_included_results = array(); // Used only in conjuntion with virtual limits for doing find('first',...include'=>...
350 $object_associates_details = array();
351 $ids = array();
352 while ($record = $results->FetchRow()) {
353 $this_item_attributes = array();
354 $associated_items = array();
355 foreach ($record as $column=>$value){
356 if(!is_numeric($column)){
357 if(substr($column,0,8) == '__owner_'){
358 $attribute_name = substr($column,8);
359 $this_item_attributes[$attribute_name] = $value;
360 }elseif(preg_match('/^_('.join('|',$associated_ids).')_(.+)/',$column, $match)){
361 $associated_items[$match[1]][$match[2]] = $value;
366 // We need to keep a pointer to unique parent elements in order to add associates to the first loaded item
367 $e = null;
368 $object_id = $this_item_attributes[$this->getPrimaryKey()];
370 if(!empty($virtual_limit)){
371 $_included_results[$object_id] = $object_id;
372 if(count($_included_results) > $virtual_limit * $number_of_associates){
373 continue;
377 if(!isset($ids[$object_id])){
378 $ids[$object_id] = $i;
379 $attributes_for_instantation = $this->getOnlyAvailableAttributes($this_item_attributes);
380 $attributes_for_instantation['load_associations'] = true;
381 $objects[$i] =& $this->instantiate($attributes_for_instantation, false);
382 }else{
383 $e = $i;
384 $i = $ids[$object_id];
387 foreach ($associated_items as $association_id=>$attributes){
388 if(count(array_diff($attributes, array(''))) > 0){
389 $object_associates_details[$i][$association_id][md5(serialize($attributes))] = $attributes;
393 $i = !is_null($e) ? $e : $i+1;
396 if(!empty($object_associates_details)){
397 foreach ($object_associates_details as $i=>$object_associate_details){
398 foreach ($object_associate_details as $association_id => $associated_attributes){
399 foreach ($associated_attributes as $attributes){
400 if(count(array_diff($attributes, array(''))) > 0){
401 if(!method_exists($objects[$i]->$association_id, 'build')){
402 $handler_name = $this->getAssociatedHandlerName($association_id);
403 $objects[$i]->$handler_name->build($attributes, false);
404 }else{
405 $objects[$i]->$association_id->build($attributes, false);
406 $objects[$i]->$association_id->_newRecord = false;
414 $result =& $objects;
415 return $result;
419 function _addTableAliasesToAssociatedSql($table_alias, $sql)
421 return preg_replace($this->getColumnsWithRegexBoundaries(),'\1'.$table_alias.'.\2',' '.$sql.' ');