Fixing file upload params ($_FILES) normalization. Closes #75
[akelos.git] / lib / AkActiveRecord / AkAssociatedActiveRecord.php
blob26f26a8d3eae0d2885d594cd7b7eeb6179eb05ba
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.$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, $limit = null, $offset = null)
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 $avaliable_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($avaliable_associated_options) as $associated_option){
258 if(!empty($associated_options[$associated_option])){
259 $avaliable_associated_options[$associated_option][] = $associated_options[$associated_option];
264 foreach ($avaliable_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));
275 $sql = substr($sql, -5) == 'AND =' ? substr($sql, 0,-5) : $sql;
277 if(!empty($options['bind']) && is_array($options['bind']) && strstr($sql,'?')){
278 $sql = array_merge(array($sql),$options['bind']);
280 $result =& $this->_findBySqlWithAssociations($sql, $limit, $offset, $options['include'], empty($options['virtual_limit']) ? false : $options['virtual_limit']);
282 return $result;
286 function getCollectionHandlerName($association_id)
288 if(isset($this->$association_id) && is_object($this->$association_id) && method_exists($this->$association_id,'getAssociatedFinderSqlOptions')){
289 return false;
291 $collection_handler_name = AkInflector::singularize($association_id);
292 if(isset($this->$collection_handler_name) &&
293 is_object($this->$collection_handler_name) &&
294 in_array($this->$collection_handler_name->getType(),array('hasMany','hasAndBelongsToMany'))){
295 return $collection_handler_name;
296 }else{
297 return false;
303 * Used for generating custom selections for habtm, has_many and has_one queries
305 function constructFinderSqlWithAssociations($options, $include_owner_as_selection = true)
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.', ';
313 $sql = 'SELECT '.trim($selection.@$options['selection'], ', ').' '.
314 'FROM '.$this->getTableName().' AS __owner '.
315 (!empty($options['joins']) ? $options['joins'].' ' : '');
316 }else{
317 $sql = 'SELECT '.$options['selection'].'.* '.
318 'FROM '.$options['selection'].' '.
319 (!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'] : '';
329 return $sql;
334 * @todo Refactor in order to increase performance of associated inclussions
336 function &_findBySqlWithAssociations($sql, $limit = null, $offset = null, $included_associations = array(), $virtual_limit = false)
338 if(is_array($sql)){
339 $sql_query = array_shift($sql);
340 $bindings = is_array($sql) && count($sql) > 0 ? $sql : array($sql);
341 $sql = $sql_query;
343 $this->setConnection();
345 AK_LOG_EVENTS ? $this->_startSqlBlockLog() : null;
347 $objects = array();
348 $_included_results = array(); // Used only in conjuntion with virtual limits for doing find('first',...include'=>...
349 if(is_integer($limit)){
350 if(is_integer($offset)){
351 $results = !empty($bindings) ?
352 $this->_db->SelectLimit($sql, $limit, $offset, $bindings) :
353 $this->_db->SelectLimit($sql, $limit, $offset);
354 }else {
355 $results = !empty($bindings) ?
356 $this->_db->SelectLimit($sql, $limit, -1, $bindings) :
357 $this->_db->SelectLimit($sql, $limit);
359 }else{
360 $results = !empty($bindings) ?
361 $this->_db->Execute($sql, $bindings) :
362 $this->_db->Execute($sql);
365 AK_LOG_EVENTS ? $this->_endSqlBlockLog() : null;
367 if(!$results && AK_DEBUG){
368 trigger_error($this->_db->ErrorMsg(), E_USER_NOTICE);
369 }else{
370 $objects = array();
371 $i = 0;
372 $associated_ids = $this->getAssociatedIds();
373 $number_of_associates = count($associated_ids);
374 $object_associates_details = array();
376 $ids = array();
377 while ($record = $results->FetchRow()) {
378 $this_item_attributes = array();
379 $associated_items = array();
380 foreach ($record as $column=>$value){
381 if(!is_numeric($column)){
382 if(substr($column,0,8) == '__owner_'){
383 $attribute_name = substr($column,8);
384 $this_item_attributes[$attribute_name] = $value;
385 }elseif(preg_match('/^_('.join('|',$associated_ids).')_(.+)/',$column, $match)){
386 $associated_items[$match[1]][$match[2]] = $value;
392 // We need to keep a pointer to unique parent elements in order to add associates to the first loaded item
393 $e = null;
394 $object_id = $this_item_attributes[$this->getPrimaryKey()];
396 if(!empty($virtual_limit)){
397 $_included_results[$object_id] = $object_id;
398 if(count($_included_results) > $virtual_limit * $number_of_associates){
399 continue;
403 if(!isset($ids[$object_id])){
404 $ids[$object_id] = $i;
405 $attributes_for_instantation = $this->getOnlyAvailableAtrributes($this_item_attributes);
406 $attributes_for_instantation['load_associations'] = true;
407 $objects[$i] =& $this->instantiate($attributes_for_instantation, false);
408 }else{
409 $e = $i;
410 $i = $ids[$object_id];
413 foreach ($associated_items as $association_id=>$attributes){
414 if(count(array_diff($attributes, array(''))) > 0){
415 $object_associates_details[$i][$association_id][md5(serialize($attributes))] = $attributes;
419 $i = !is_null($e) ? $e : $i+1;
423 if(!empty($object_associates_details)){
424 foreach ($object_associates_details as $i=>$object_associate_details){
425 foreach ($object_associate_details as $association_id => $associated_attributes){
426 foreach ($associated_attributes as $attributes){
427 if(count(array_diff($attributes, array(''))) > 0){
428 if(!method_exists($objects[$i]->$association_id, 'build')){
429 $handler_name = $this->getAssociatedHandlerName($association_id);
430 $objects[$i]->$handler_name->build($attributes, false);
431 }else{
432 $objects[$i]->$association_id->build($attributes, false);
433 $objects[$i]->$association_id->_newRecord = false;
441 if(!empty($objects)){
442 $result =& $objects;
443 }else{
444 $result = false;
447 return $result;
451 function _addTableAliasesToAssociatedSql($table_alias, $sql)
453 return preg_replace($this->getColumnsWithRegexBoundaries(),'\1'.$table_alias.'.\2',' '.$sql.' ');