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
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');
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;
81 if(!empty($association_details) && $this->_loadAssociationHandler($association_type)){
82 $this->$association_type->initializeAssociated($association_details);
83 $this->_associations
[$association_type] =& $this->$association_type;
89 * Gets an array of associated object of selected association type.
91 function &getAssociated($association_type)
94 if(!empty($this->$association_type) && in_array($association_type, array('hasOne','belongsTo','hasMany','hasAndBelongsToMany'))){
95 $result =& $this->$association_type->getModels();
106 function &assign(&$Associated)
109 if(is_object($this->_AssociationHandler
)){
110 $result =& $this->_AssociationHandler
->assign($this->getAssociationId(), $Associated);
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)
122 if(!empty($this->_AssociationHandler
)){
123 $result =& $this->_AssociationHandler
->build($this->getAssociationId(), $attributes, $replace_existing);
129 function &create($attributes = array(), $replace_existing = true)
132 if(!empty($this->_AssociationHandler
)){
133 $result =& $this->_AssociationHandler
->create($this->getAssociationId(), $attributes, $replace_existing);
138 function &replace(&$NewAssociated, $dont_save = false)
141 if(!empty($this->_AssociationHandler
)){
142 $result =& $this->_AssociationHandler
->replace($this->getAssociationId(), $NewAssociated, $dont_save = false);
150 if(!empty($this->_AssociationHandler
)){
151 $result =& $this->_AssociationHandler
->findAssociated($this->getAssociationId());
159 if(!empty($this->_AssociationHandler
)){
160 $result =& $this->_AssociationHandler
->loadAssociated($this->getAssociationId());
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();
222 return $this->getAssociatedType();
226 function hasAssociations()
228 return !empty($this->_associations
) && count($this->_associations
) > 0;
231 function &findWithAssociations($options)
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){
242 $included_associations[] = $v;
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){
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']);
285 function getCollectionHandlerName($association_id)
287 if(isset($this->$association_id) && is_object($this->$association_id) && method_exists($this->$association_id,'getAssociatedFinderSqlOptions')){
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;
302 * Used for generating custom selections for habtm, has_many and has_one queries
304 function constructFinderSqlWithAssociations($options, $include_owner_as_selection = true)
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
315 // used only by HasOne::findAssociated
316 $selection .= $options['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);
336 * @todo Refactor in order to increase performance of associated inclussions
338 function &_findBySqlWithAssociations($sql, $included_associations = array(), $virtual_limit = false)
341 $results = $this->_db
->execute ($sql,'find with associations');
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();
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
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){
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);
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);
405 $objects[$i]->$association_id->build($attributes, false);
406 $objects[$i]->$association_id->_newRecord
= false;
419 function _addTableAliasesToAssociatedSql($table_alias, $sql)
421 return preg_replace($this->getColumnsWithRegexBoundaries(),'\1'.$table_alias.'.\2',' '.$sql.' ');