[ Index ]

PHP Cross Reference of Akelos Framework

title

Body

[close]

/AkActiveRecord/AkAssociations/ -> AkHasAndBelongsToMany.php (source)

   1  <?php
   2  /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
   3  
   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  // +----------------------------------------------------------------------+
  10  
  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>
  17   */
  18  
  19  defined('AK_HAS_AND_BELONGS_TO_MANY_CREATE_JOIN_MODEL_CLASSES') ? null : define('AK_HAS_AND_BELONGS_TO_MANY_CREATE_JOIN_MODEL_CLASSES' ,true);
  20  defined('AK_HAS_AND_BELONGS_TO_MANY_JOIN_CLASS_EXTENDS') ? null : define('AK_HAS_AND_BELONGS_TO_MANY_JOIN_CLASS_EXTENDS' , 'ActiveRecord');
  21  
  22  require_once (AK_LIB_DIR.DS.'AkActiveRecord'.DS.'AkAssociation.php');
  23  
  24  /**
  25  * Associates two classes via an intermediate join table.  Unless the join table is explicitly specified as
  26  * an option, it is guessed using the lexical order of the class names. So a join between Developer and Project
  27  * will give the default join table name of "developers_projects" because "D" outranks "P".
  28  *
  29  * Adds the following methods for retrieval and query.
  30  * 'collection' is replaced with the associton identification passed as the first argument, so 
  31  * <tt>var $has_and_belongs_to_many = 'categories'</tt> would make available on the its parent an array of
  32  *  objects on $this->categories and a collection handling interface instance on $this->category (singular form)
  33  * * <tt>collection->load($force_reload = false)</tt> - returns an array of all the associated objects.
  34  *   An empty array is returned if none is found.
  35  * * <tt>collection->add($object, ...)</tt> - adds one or more objects to the collection by creating associations in the join table 
  36  *   (collection->push and $collection->concat are aliases to this method).
  37  * * <tt>collection->pushWithAttributes($object, $join_attributes)</tt> - adds one to the collection by creating an association in the join table that
  38  *   also holds the attributes from <tt>join_attributes</tt> (should be an array with the column names as keys). This can be used to have additional
  39  *   attributes on the join, which will be injected into the associated objects when they are retrieved through the collection.
  40  *   (collection->concatWithAttributes() is an alias to this method).
  41  * * <tt>collection->delete($object, ...)</tt> - removes one or more objects from the collection by removing their associations from the join table.  
  42  *   This does not destroy the objects.
  43  * * <tt>collection->set($objects)</tt> - replaces the collections content by deleting and adding objects as appropriate.
  44  * * <tt>collection->setByIds($ids)</tt> - replace the collection by the objects identified by the primary keys in $ids
  45  * * <tt>collection->clear()</tt> - removes every object from the collection. This does not destroy the objects.
  46  * * <tt>collection->isEmpty()</tt> - returns true if there are no associated objects.
  47  * * <tt>collection->size()</tt> - returns the number of associated objects. (collection->count() is an alias to this method)
  48  * * <tt>collection->find($id)</tt> - finds an associated object responding to the +id+ and that
  49  *   meets the condition that it has to be associated with this object.
  50  *
  51  * Example: A Developer class declares <tt>var $has_and_belongs_to_many = 'projects'</tt>, which will add:
  52  * * <tt>Developer->projects</tt> (The collection)
  53  * * <tt>Developer->project</tt> (The association manager)
  54  * * <tt>Developer->project->add()<<</tt>
  55  * * <tt>Developer->project->pushWithAttributes()</tt>
  56  * * <tt>Developer->project->delete()</tt>
  57  * * <tt>Developer->project->set()</tt>
  58  * * <tt>Developer->project->setByIds()</tt>
  59  * * <tt>Developer->project->clear()</tt>
  60  * * <tt>Developer->projects->isEmpty()</tt>
  61  * * <tt>Developer->projects->size()</tt>
  62  * * <tt>Developer->projects->find($id)</tt>
  63  * 
  64  * The declaration may include an options hash to specialize the behavior of the association.
  65  * 
  66  * Options are:
  67  * * <tt>class_name</tt> - specify the class name of the association. Use it only if that name can't be inferred
  68  *   from the association name. So <tt>var $has_and_belongs_to_many = 'projects'</tt> will by default be linked to the 
  69  *   Project class, but if the real class name is SuperProject, you'll have to specify it with this option.
  70  * * <tt>table_name</tt> - Name for the associated object database table. As this association will not instantiate the associated model
  71  * it nees to know the name of the table we are going to join. This is infered form previous class_name
  72  * * <tt>join_table</tt> - specify the name of the join table if the default based on lexical order isn't what you want.
  73  *   WARNING: If you're overwriting the table name of either class, the table_name method MUST be declared underneath any
  74  *   $has_and_belongs_to_many declaration in order to work.
  75  * * <tt>join_class_name</tt> - specify the class name of the association join table. If the class does not exist, a new class
  76  * will be created on runtime to load the results into the collection. The class name will be infered from the join_table name
  77  * * <tt>foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
  78  *   of this class in lower-case and "_id" suffixed. So a Person class that makes a $has_and_belongs_to_many association
  79  *   will use "person_id" as the default foreign_key.
  80  * * <tt>association_foreign_key</tt> - specify the association foreign key used for the association. By default this is
  81  *   guessed to be the name of the associated class in lower-case and "_id" suffixed. So if the associated class is Project,
  82  *   the has_and_belongs_to_many association will use "project_id" as the default association foreign_key.
  83  * * <tt>conditions</tt>  - specify the conditions that the associated object must meet in order to be included as a "WHERE"
  84  *   sql fragment, such as "authorized = 1".
  85  * * <tt>order</tt> - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment, such as "last_name, first_name DESC"
  86  * * <tt>finder_sql</tt> - overwrite the default generated SQL used to fetch the association with a manual one
  87  * * <tt>delete_sql</tt> - overwrite the default generated SQL used to remove links between the associated 
  88  *   classes with a manual one
  89  * * <tt>insert_sql</tt> - overwrite the default generated SQL used to add links between the associated classes
  90  *   with a manual one
  91  * * <tt>include</tt>  - specify second-order associations that should be eager loaded when the collection is loaded.
  92  * * <tt>group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
  93  * * <tt>limit</tt>: An integer determining the limit on the number of rows that should be returned.
  94  * * <tt>offset</tt>: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
  95  * * <tt>select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not
  96  *   include the joined columns.
  97  * * <tt>unique</tt> - if set to true, duplicates will be omitted from the collection.
  98  *
  99  * Option examples:
 100  *   $has_and_belongs_to_many = 'projects';
 101  *   $has_and_belongs_to_many = array('projects'=> array('include' => array('milestones', 'manager')))
 102  *   $has_and_belongs_to_many = array('nations' => array('class_name' => "Country"))
 103  *   $has_and_belongs_to_many = array('categories' => array('join_table' => "prods_cats"))
 104  *   $has_and_belongs_to_many = array('active_projects' => array('join_table' => 'developers_projects', 'delete_sql' => 
 105  *   'DELETE FROM developers_projects WHERE active=1 AND developer_id = $id AND project_id = $record->id'
 106  */
 107  class AkHasAndBelongsToMany extends AkAssociation
 108  {
 109      /**
 110       * Join object place holder
 111       */
 112      var $JoinObject;
 113      var $associated_ids = array();
 114      var $association_id;
 115      var $_automatically_create_join_model_files = AK_HAS_AND_BELONGS_TO_MANY_CREATE_JOIN_MODEL_CLASSES;
 116  
 117      function &addAssociated($association_id, $options = array())
 118      {
 119          $default_options = array(
 120          'class_name' => empty($options['class_name']) ? AkInflector::classify($association_id) : $options['class_name'],
 121          'table_name' => false,
 122          'join_table' => false,
 123          'join_class_name' => false,
 124          'foreign_key' => false,
 125          'association_foreign_key' => false,
 126          'conditions' => false,
 127          'order' => false,
 128          'join_class_extends' => AK_HAS_AND_BELONGS_TO_MANY_JOIN_CLASS_EXTENDS,
 129          'join_class_primary_key' => 'id', // Used for removing items from the collection
 130          'finder_sql' => false,
 131          'delete_sql' => false,
 132          'insert_sql' => false,
 133          'include' => false,
 134          'group' => false,
 135          'limit' => false,
 136          'offset' => false,
 137          'handler_name' => strtolower(AkInflector::underscore(AkInflector::singularize($association_id))),
 138          'select' => false,
 139          'instantiate' => false,
 140          'unique' => false
 141          /*
 142          'include_conditions_when_included' => true,
 143          'include_order_when_included' => true,
 144          'counter_sql' => false,
 145          */
 146  
 147          );
 148  
 149          $options = array_merge($default_options, $options);
 150  
 151          $owner_name = $this->Owner->getModelName();
 152          $owner_table = $this->Owner->getTableName();
 153          $associated_name = $options['class_name'];
 154          $associated_table_name = $options['table_name'] = (empty($options['table_name']) ? AkInflector::tableize($associated_name) : $options['table_name']);
 155  
 156          $join_tables = array($owner_table, $associated_table_name);
 157          sort($join_tables);
 158  
 159          $options['join_table'] = empty($options['join_table']) ? join('_', $join_tables) : $options['join_table'];
 160          $options['join_class_name'] = empty($options['join_class_name']) ? join(array_map(array('AkInflector','classify'),array_map(array('AkInflector','singularize'), $join_tables))) : $options['join_class_name'];
 161          $options['foreign_key'] = empty($options['foreign_key']) ? AkInflector::underscore($owner_name).'_id' : $options['foreign_key'];
 162          $options['association_foreign_key'] = empty($options['association_foreign_key']) ? AkInflector::underscore($associated_name).'_id' : $options['association_foreign_key'];
 163  
 164          $Collection =& $this->_setCollectionHandler($association_id, $options['handler_name']);
 165          $Collection->setOptions($association_id, $options);
 166  
 167          $this->addModel($association_id,  $Collection);
 168  
 169          if($options['instantiate']){
 170              $associated =& $Collection->load();
 171          }
 172  
 173          $this->setAssociatedId($association_id, $options['handler_name']);
 174          $Collection->association_id = $association_id;
 175  
 176          $Collection->_loadJoinObject();
 177  
 178          return $Collection;
 179      }
 180  
 181      function getType()
 182      {
 183          return 'hasAndBelongsToMany';
 184      }
 185  
 186      function &_setCollectionHandler($association_id, $handler_name)
 187      {
 188          $false = false;
 189          if(isset($this->Owner->$association_id)){
 190              if(!is_array($this->Owner->$association_id)){
 191                  trigger_error(Ak::t('%model_name::%association_id is not a collection array on current %association_id hasAndBelongsToMany association',array('%model_name'=>$this->Owner->getModelName(), '%association_id'=>$association_id)), E_USER_NOTICE);
 192              }
 193              $associated =& $this->Owner->$association_id;
 194          }else{
 195              $associated = array();
 196              $this->Owner->$association_id =& $associated;
 197          }
 198  
 199          if(isset($this->Owner->$handler_name)){
 200              trigger_error(Ak::t('Could not load %association_id on %model_name because "%model_name->%handler_name" attribute '.
 201              'is already defined and can\'t be used as an association placeholder',
 202              array('%model_name'=>$this->Owner->getModelName(),'%association_id'=>$association_id, '%handler_name'=>$handler_name)),
 203              E_USER_ERROR);
 204              return $false;
 205          }else{
 206              $this->Owner->$handler_name =& new AkHasAndBelongsToMany($this->Owner);
 207          }
 208          return $this->Owner->$handler_name;
 209      }
 210  
 211  
 212      // We are going to load or create the join class
 213      function _loadJoinObject()
 214      {
 215          if($this->_isJoinObjectLoaded()){
 216              return true;
 217          }
 218          $options = $this->getOptions($this->association_id);
 219  
 220          if (class_exists($options['join_class_name']) || $this->_loadJoinClass($options['join_class_name']) || $this->_createJoinClass()) {
 221              $this->JoinObject =& new $options['join_class_name']();
 222              if($this->_tableExists($options['join_table']) || $this->_createJoinTable()){
 223                  $this->JoinObject->setPrimaryKey($options['foreign_key']);
 224                  return true;
 225  
 226              } else {
 227                  trigger_error(Ak::t('Could not find join table %table_name for hasAndBelongsToMany association %id',array('%table_name'=>$options['join_table'],'id'=>$this->association_id)),E_USER_ERROR);
 228              }
 229          } else {
 230              trigger_error(Ak::t('Could not find join model %model_name for hasAndBelongsToMany association %id',array('%table_name'=>$options['join_class_name'],'id'=>$this->association_id)),E_USER_ERROR); return false;
 231          }
 232          return false;
 233      }
 234  
 235      function _isJoinObjectLoaded()
 236      {
 237          $options = $this->getOptions($this->association_id);
 238          return !empty($this->JoinObject) && (strtolower($options['join_class_name']) == strtolower(get_class($this->JoinObject)));
 239      }
 240  
 241      function _loadJoinClass($class_name)
 242      {
 243          $model_file = AkInflector::toModelFilename($class_name);
 244          return file_exists($model_file) && require_once($model_file);
 245      }
 246  
 247      function _createJoinClass()
 248      {
 249          $options = $this->getOptions($this->association_id);
 250  
 251          $class_file_code = "<?php \n\n//This code was generated automatically by the active record hasAndBelongsToMany Method\n\n";
 252          $class_code =
 253          "class {$options['join_class_name']} extends {$options['join_class_extends']} {
 254      var \$_avoidTableNameValidation = true;
 255      function {$options['join_class_name']}()
 256      {
 257          \$this->setModelName(\"{$options['join_class_name']}\");
 258          \$attributes = (array)func_get_args();
 259          \$this->setTableName('{$options['join_table']}', true, true);
 260          \$this->init(\$attributes);
 261      }
 262  }";
 263          $class_file_code .= $class_code. "\n\n?>";
 264          $join_file = AkInflector::toModelFilename($options['join_class_name']);
 265          if($this->_automatically_create_join_model_files && !file_exists($join_file) && @Ak::file_put_contents($join_file, $class_file_code)){
 266              require_once($join_file);
 267          }else{
 268              eval($class_code);
 269          }
 270          return class_exists($options['join_class_name']);
 271      }
 272  
 273      function _tableExists($table_name)
 274      {
 275          return $this->JoinObject->setTableName($table_name, true, true);
 276      }
 277  
 278      function _createJoinTable()
 279      {
 280          $options = $this->getOptions($this->association_id);
 281          require_once (AK_LIB_DIR.DS.'AkInstaller.php');
 282          $Installer =& new AkInstaller();
 283          $Installer->createTable($options['join_table'],"id,{$options['foreign_key']},{$options['association_foreign_key']}",array('timestamp'=>false));
 284          return $this->JoinObject->setTableName($options['join_table'],false);
 285      }
 286  
 287      function &load($force_reload = false)
 288      {
 289          $options = $this->getOptions($this->association_id);
 290          if($force_reload || empty($this->Owner->{$options['handler_name']}->_loaded)){
 291              if(!$this->Owner->isNewRecord()){
 292                  $this->constructSql();
 293                  $options = $this->getOptions($this->association_id);
 294                  $Associated =& $this->getAssociatedModelInstance();
 295                  if($FoundAssociates = $Associated->findBySql($options['finder_sql'])){
 296                      array_map(array(&$this,'_setAssociatedMemberId'),$FoundAssociates);
 297                      $this->Owner->{$this->association_id} =& $FoundAssociates;
 298                  }
 299              }
 300              if(empty($this->Owner->{$this->association_id})){
 301                  $this->Owner->{$this->association_id} = array();
 302              }
 303  
 304              $this->Owner->{$options['handler_name']}->_loaded = true;
 305          }
 306          return $this->Owner->{$this->association_id};
 307      }
 308  
 309  
 310      /**
 311       * add($object), add(array($object, $object2)) - adds one or more objects to the collection by setting 
 312       * their foreign keys to the collection?s primary key. Items are saved automatically when parent has been saved.
 313       */
 314      function add(&$Associated)
 315      {
 316          if(is_array($Associated)){
 317              $external_key = '__associated_to_model_'.$this->Owner->getModelName().'_as_'.$this->association_id;
 318              $succes = true;
 319              $succes = $this->Owner->notifyObservers('beforeAdd') ? $succes : false;
 320              $options = $this->getOptions($this->association_id);
 321              foreach (array_keys($Associated) as $k){
 322                  if(empty($Associated[$k]->$external_key) && !$this->_hasAssociatedMember($Associated[$k])){
 323                      if(!empty($options['before_add']) && method_exists($this->Owner, $options['before_add']) && $this->Owner->{$options['before_add']}($Associated[$k]) === false ){
 324                          $succes = false;
 325                      }else{
 326                          $Associated[$k]->$external_key = $Associated[$k]->isNewRecord();
 327                          $this->Owner->{$this->association_id}[] =& $Associated[$k];
 328                          $this->_setAssociatedMemberId($Associated[$k]);
 329                          if($this->_relateAssociatedWithOwner($Associated[$k])){
 330                              if($succes && !empty($options['after_add']) && method_exists($this->Owner, $options['after_add']) && $this->Owner->{$options['after_add']}($Associated[$k]) === false ){
 331                                  $succes = false;
 332                              }
 333                          }else{
 334                              $succes = false;
 335                          }
 336                      }
 337                  }
 338              }
 339              $succes = $this->Owner->notifyObservers('afterAdd') ? $succes : false;
 340              return $succes;
 341          }else{
 342              $associates = array();
 343              $associates[] =& $Associated;
 344              return $this->add($associates);
 345          }
 346      }
 347  
 348  
 349      function push(&$record)
 350      {
 351          return $this->add($record);
 352      }
 353  
 354      function concat(&$record)
 355      {
 356          return $this->add($record);
 357      }
 358  
 359      /**
 360      * Remove all records from this association
 361      */
 362      function deleteAll()
 363      {
 364          $this->load();
 365          return $this->delete($this->Owner->{$this->association_id});
 366      }
 367  
 368      function reset()
 369      {
 370          $options = $this->getOptions($this->association_id);
 371          $this->Owner->{$options['handler_name']}->_loaded = false;
 372      }
 373  
 374      function set(&$objects)
 375      {
 376          $this->deleteAll($objects);
 377          $this->add($objects);
 378      }
 379  
 380      function setIds()
 381      {
 382          $ids = func_get_args();
 383          $ids = is_array($ids[0]) ? $ids[0] : $ids;
 384          $AssociatedModel =& $this->getAssociatedModelInstance();
 385          if(!empty($ids)){
 386              $NewAssociates =& $AssociatedModel->find($ids);
 387              $this->set($NewAssociates);
 388          }
 389      }
 390  
 391      function setByIds()
 392      {
 393          $ids = func_get_args();
 394          call_user_func_array(array($this,'setIds'), $ids);
 395      }
 396  
 397      function addId($id)
 398      {
 399          $AssociatedModel =& $this->getAssociatedModelInstance();
 400          if($NewAssociated =& $AssociatedModel->find($id)){
 401              return $this->add($NewAssociated);
 402          }
 403          return false;
 404      }
 405  
 406  
 407      function delete(&$Associated)
 408      {
 409          $success = true;
 410          if(!is_array($Associated)){
 411              $associated_elements = array();
 412              $associated_elements[] =& $Associated;
 413              return $this->delete($associated_elements);
 414          }else{
 415              $options = $this->getOptions($this->association_id);
 416  
 417              $ids_to_nullify = array();
 418              $ids_to_delete = array();
 419              $items_to_remove_from_collection = array();
 420              $AssociatedModel =& $this->getAssociatedModelInstance();
 421  
 422              $owner_type = $this->_findOwnerTypeForAssociation($AssociatedModel, $this->Owner);
 423  
 424              $this->JoinObject->setPrimaryKey($options['join_class_primary_key']);
 425  
 426              foreach (array_keys($Associated) as $k){
 427                  $id = $Associated[$k]->getId();
 428                  if($JoinObjectsToDelete =& $this->JoinObject->findAllBy($options['foreign_key'].' AND '.$options['association_foreign_key'], $this->Owner->getId(), $id)){
 429                      foreach (array_keys($JoinObjectsToDelete) as $k) {
 430                          if($JoinObjectsToDelete[$k]->destroy()){
 431                              $items_to_remove_from_collection[] = $id;
 432                          }else{
 433                              $success = false;
 434                          }
 435                      }
 436                  }
 437              }
 438  
 439              $this->JoinObject->setPrimaryKey($options['foreign_key']);
 440  
 441              $success ? $this->removeFromCollection($items_to_remove_from_collection) : null;
 442          }
 443  
 444          return $success;
 445      }
 446  
 447  
 448  
 449      /**
 450      * Remove records from the collection. Use delete() in order to trigger database dependencies
 451      */
 452      function removeFromCollection(&$records)
 453      {
 454          if(!is_array($records)){
 455              $records_array = array();
 456              $records_array[] =& $records;
 457              $this->delete($records_array);
 458          }else{
 459              $this->Owner->notifyObservers('beforeRemove');
 460              $options = $this->getOptions($this->association_id);
 461              foreach (array_keys($records) as $k){
 462                  if(!empty($options['before_remove']) && method_exists($this->Owner, $options['before_remove']) && $this->Owner->{$options['before_remove']}($records[$k]) === false ){
 463                      continue;
 464                  }
 465  
 466                  if(isset($records[$k]->__activeRecordObject)){
 467                      $record_id = $records[$k]->getId();
 468                  }else{
 469                      $record_id = $records[$k];
 470                  }
 471                  
 472                  foreach (array_keys($this->Owner->{$this->association_id}) as $kk){
 473                      if(
 474                      (
 475                      !empty($this->Owner->{$this->association_id}[$kk]->__hasAndBelongsToManyMemberId) &&
 476                      !empty($records[$k]->__hasAndBelongsToManyMemberId) &&
 477                      $records[$k]->__hasAndBelongsToManyMemberId == $this->Owner->{$this->association_id}[$kk]->__hasAndBelongsToManyMemberId
 478                      ) || (
 479                      !empty($this->Owner->{$this->association_id}[$kk]->__activeRecordObject) &&
 480                      $record_id == $this->Owner->{$this->association_id}[$kk]->getId()
 481                      )
 482                      ){
 483                          unset($this->Owner->{$this->association_id}[$kk]);
 484                      }
 485                  }
 486                  unset($this->associated_ids[$record_id]);
 487                  $this->_unsetAssociatedMemberId($records[$k]);
 488                  if(!empty($options['after_remove']) && method_exists($this->Owner, $options['after_remove'])){
 489                      $this->Owner->{$options['after_remove']}($records[$k]);
 490                  }
 491              }
 492              $this->Owner->notifyObservers('afterRemove');
 493          }
 494      }
 495  
 496  
 497  
 498  
 499      function _setAssociatedMemberId(&$Member)
 500      {
 501          if(empty($Member->__hasAndBelongsToManyMemberId)) {
 502              $Member->__hasAndBelongsToManyMemberId = Ak::randomString();
 503          }
 504          $object_id = $Member->getId();
 505          if(!empty($object_id)){
 506              $this->associated_ids[$object_id] = $Member->__hasAndBelongsToManyMemberId;
 507          }
 508      }
 509  
 510      function _unsetAssociatedMemberId(&$Member)
 511      {
 512          $id = $this->_getAssociatedMemberId($Member);
 513          unset($this->associated_ids[$id]);
 514          unset($Member->__hasAndBelongsToManyMemberId);
 515      }
 516  
 517      function _getAssociatedMemberId(&$Member)
 518      {
 519          if(!empty($Member->__hasAndBelongsToManyMemberId)) {
 520              return array_search($Member->__hasAndBelongsToManyMemberId, $this->associated_ids);
 521          }
 522          return false;
 523      }
 524  
 525      function _hasAssociatedMember(&$Member)
 526      {
 527          $options = $this->getOptions($this->association_id);
 528          if($options['unique'] && !$Member->isNewRecord() && isset($this->associated_ids[$Member->getId()])){
 529              return true;
 530          }
 531          $id = $this->_getAssociatedMemberId($Member);
 532          return !empty($id);
 533      }
 534  
 535  
 536      function _relateAssociatedWithOwner(&$Associated)
 537      {
 538          if(!$this->Owner->isNewRecord()){
 539              $success = true;
 540              $options = $this->getOptions($this->association_id);
 541              if(strtolower($options['join_class_name']) != strtolower(get_class($this->JoinObject))){
 542                  return false;
 543              }
 544              if($Associated->isNewRecord() ? $Associated->save() : true){
 545                  if(!$this->_getAssociatedMemberId($Associated)){
 546                      $this->_setAssociatedMemberId($Associated);
 547                  }
 548  
 549                  $foreign_key = $this->Owner->getId();
 550                  $association_foreign_key = $Associated->getId();
 551                  if($foreign_key != $this->JoinObject->get($options['foreign_key']) ||
 552                  $association_foreign_key != $this->JoinObject->get($options['association_foreign_key'])){
 553  
 554                      $this->JoinObject =& $this->JoinObject->create(array($options['foreign_key']=> $foreign_key, $options['association_foreign_key']=> $association_foreign_key));
 555                      $success = !$this->JoinObject->isNewRecord();
 556                  }
 557              }
 558              if($success){
 559                  $Associated->hasAndBelongsToMany->__joined = true;
 560              }
 561              return $success;
 562          }
 563          return false;
 564      }
 565  
 566      function &_build($association_id, &$AssociatedObject, $reference_associated = true)
 567      {
 568          if($reference_associated){
 569              $this->Owner->$association_id =& $AssociatedObject;
 570          }else{
 571              $this->Owner->$association_id = $AssociatedObject;
 572          }
 573          $this->Owner->$association_id->_AssociationHandler =& $this;
 574          $this->Owner->$association_id->_associatedAs = $this->getType();
 575          $this->Owner->$association_id->_associationId = $association_id;
 576          $this->Owner->_associations[$association_id] =& $this->Owner->$association_id;
 577          return $this->Owner->$association_id;
 578      }
 579  
 580  
 581  
 582  
 583      function constructSql()
 584      {
 585          $options = $this->getOptions($this->association_id);
 586          if(empty($options['finder_sql'])){
 587              $is_sqlite = $this->Owner->_db->type() == 'sqlite';
 588              $options['finder_sql'] = "SELECT {$options['table_name']}.* FROM {$options['table_name']} ".
 589              $this->associationJoin().
 590              "WHERE ".$this->Owner->getTableName().'.'.$this->Owner->getPrimaryKey()." ".
 591              ($is_sqlite ? ' LIKE ' : ' = ').' '.$this->Owner->quotedId(); // (HACK FOR SQLITE) Otherwise returns wrong data
 592              $options['finder_sql'] .= !empty($options['conditions']) ? ' AND '.$options['conditions'].' ' : '';
 593              $options['finder_sql'] .= !empty($options['conditions']) ? ' AND '.$options['conditions'].' ' : '';
 594          }
 595          if(empty($options['counter_sql'])){
 596              $options['counter_sql'] = substr_replace($options['finder_sql'],'SELECT COUNT(*)',0,strpos($options['finder_sql'],'*')+1);
 597          }
 598          $this->setOptions($this->association_id, $options);
 599      }
 600  
 601      function associationJoin()
 602      {
 603          $Associated =& $this->getAssociatedModelInstance();
 604          $options = $this->getOptions($this->association_id);
 605  
 606          return "LEFT OUTER JOIN {$options['join_table']} ON ".
 607          "{$options['join_table']}.{$options['association_foreign_key']} = {$options['table_name']}.".$Associated->getPrimaryKey()." ".
 608          "LEFT OUTER JOIN ".$this->Owner->getTableName()." ON ".
 609          "{$options['join_table']}.{$options['foreign_key']} = ".$this->Owner->getTableName().".".$this->Owner->getPrimaryKey()." ";
 610      }
 611  
 612  
 613  
 614      function count()
 615      {
 616          $count = 0;
 617          $options = $this->getOptions($this->association_id);
 618          if(empty($this->Owner->{$options['handler_name']}->_loaded) && !$this->Owner->isNewRecord()){
 619              $this->constructSql();
 620              $options = $this->getOptions($this->association_id);
 621              $Associated =& $this->getAssociatedModelInstance();
 622  
 623              if($this->_hasCachedCounter()){
 624                  $count = $Associated->getAttribute($this->_getCachedCounterAttributeName());
 625              }elseif(!empty($options['counter_sql'])){
 626                  $count = $Associated->countBySql($options['counter_sql']);
 627              }else{
 628                  $count = (strtoupper(substr($options['finder_sql'],0,6)) != 'SELECT') ?
 629                  $Associated->count($options['foreign_key'].'='.$this->Owner->quotedId()) :
 630                  $Associated->countBySql($options['finder_sql']);
 631              }
 632          }else{
 633              $count = count($this->Owner->{$this->association_id});
 634          }
 635  
 636          if($count == 0){
 637              $this->Owner->{$this->association_id} = array();
 638              $this->Owner->{$options['handler_name']}->_loaded = true;
 639          }
 640  
 641          return $count;
 642      }
 643  
 644  
 645      function &build($attributes = array(), $set_as_new_record = true)
 646      {
 647          $options = $this->getOptions($this->association_id);
 648          Ak::import($options['class_name']);
 649          $record =& new $options['class_name']($attributes);
 650          $record->_newRecord = $set_as_new_record;
 651          $this->Owner->{$this->association_id}[] =& $record;
 652          $this->_setAssociatedMemberId($record);
 653          $set_as_new_record ? $this->_relateAssociatedWithOwner($record) : null;
 654          return $record;
 655      }
 656  
 657      function &create($attributes = array())
 658      {
 659          $record =& $this->build($attributes);
 660          if(!$this->Owner->isNewRecord()){
 661              $record->save();
 662          }
 663          return $record;
 664      }
 665  
 666  
 667      function getAssociatedFinderSqlOptions($association_id, $options = array())
 668      {
 669          $options = $this->getOptions($this->association_id);
 670          $Associated =& $this->getAssociatedModelInstance();
 671          $table_name = $Associated->getTableName();
 672          $owner_id = $this->Owner->quotedId();
 673  
 674          $finder_options = array();
 675  
 676          foreach ($options as $option=>$value) {
 677              if(!empty($value)){
 678                  $finder_options[$option] = trim($Associated->_addTableAliasesToAssociatedSql('_'.$this->association_id, $value));
 679              }
 680          }
 681  
 682          $finder_options['joins'] = $this->constructSqlForInclusion();
 683          $finder_options['selection'] = '';
 684  
 685          foreach (array_keys($Associated->getColumns()) as $column_name){
 686              $finder_options['selection'] .= '_'.$this->association_id.'.'.$column_name.' AS _'.$this->association_id.'_'.$column_name.', ';
 687          }
 688  
 689          $finder_options['selection'] = trim($finder_options['selection'], ', ');
 690  
 691          /**
 692           * @todo Refactorize me. This is too confusing
 693           */
 694          $finder_options['conditions'] =
 695          // We add previous conditions
 696          (!empty($options['conditions']) ?
 697          ' AND '.$Associated->_addTableAliasesToAssociatedSql('_'.$this->association_id, $options['conditions']).' ' : '');
 698  
 699          return $finder_options;
 700      }
 701  
 702      function constructSqlForInclusion()
 703      {
 704          $Associated =& $this->getAssociatedModelInstance();
 705          $options = $this->getOptions($this->association_id);
 706          return
 707          ' LEFT OUTER JOIN '.
 708          $options['join_table'].' AS _'.$options['join_class_name'].
 709          ' ON '.
 710          '__owner.'.$this->Owner->getPrimaryKey().
 711          ' = '.
 712          '_'.$options['join_class_name'].'.'.$options['foreign_key'].
 713  
 714          ' LEFT OUTER JOIN '.
 715          $options['table_name'].' AS _'.$this->association_id.
 716          ' ON '.
 717          '_'.$this->association_id.'.'.$Associated->getPrimaryKey().
 718          ' = '.
 719          '_'.$options['join_class_name'].'.'.$options['association_foreign_key'].' ';
 720  
 721      }
 722  
 723  
 724      function _hasCachedCounter()
 725      {
 726          $Associated =& $this->getAssociatedModelInstance();
 727          return $Associated->isAttributePresent($this->_getCachedCounterAttributeName());
 728      }
 729  
 730      function _getCachedCounterAttributeName()
 731      {
 732          return $this->association_id.'_count';
 733      }
 734  
 735  
 736      function &getAssociatedModelInstance()
 737      {
 738          static $ModelInstances;
 739          $class_name = $this->getOption($this->association_id, 'class_name');
 740          if(empty($ModelInstances[$class_name])){
 741              Ak::import($class_name);
 742              $ModelInstances[$class_name] =& new $class_name();
 743          }
 744          return $ModelInstances[$class_name];
 745      }
 746  
 747  
 748      function find()
 749      {
 750          $result = false;
 751          if(!$this->Owner->isNewRecord()){
 752              $this->constructSql();
 753              $has_and_belongs_to_many_options = $this->getOptions($this->association_id);
 754              $Associated =& $this->getAssociatedModelInstance();
 755  
 756              $args = func_get_args();
 757              $num_args = func_num_args();
 758  
 759              if(!empty($args[$num_args-1]) && is_array($args[$num_args-1])){
 760                  $options_in_args = true;
 761                  $options = $args[$num_args-1];
 762              }else{
 763                  $options_in_args = false;
 764                  $options = array();
 765              }
 766  
 767              $options['conditions'] = empty($options['conditions']) ? @$has_and_belongs_to_many_options['finder_sql'] :
 768              (empty($has_and_belongs_to_many_options['finder_sql'])  || strstr($options['conditions'], $has_and_belongs_to_many_options['finder_sql'])
 769              ? $options['conditions'] : $options['conditions'].' AND '.$has_and_belongs_to_many_options['finder_sql']);
 770  
 771              $options['order'] = empty($options['order']) ? @$has_and_belongs_to_many_options['order'] : $options['order'];
 772  
 773              $options['select_prefix'] = '';
 774  
 775              if($options_in_args){
 776                  $args[$num_args-1] = $options;
 777              }else{
 778                  $args = empty($args) ? array('all') : $args;
 779                  array_push($args, $options);
 780              }
 781  
 782              $result =& Ak::call_user_func_array(array(&$Associated,'find'), $args);
 783          }
 784  
 785          return $result;
 786      }
 787  
 788  
 789      function isEmpty()
 790      {
 791          return $this->count() === 0;
 792      }
 793  
 794      function getSize()
 795      {
 796          return $this->count();
 797      }
 798  
 799      function clear()
 800      {
 801          return $this->deleteAll();
 802      }
 803  
 804      /**
 805      * Triggers
 806      */
 807      function afterCreate(&$object)
 808      {
 809          return $this->_afterCallback($object);
 810      }
 811  
 812      function afterUpdate(&$object)
 813      {
 814          return $this->_afterCallback($object);
 815      }
 816  
 817  
 818      function beforeDestroy(&$object)
 819      {
 820          $success = true;
 821  
 822          foreach ((array)$object->_associationIds as $k => $v){
 823              if(isset($object->$k) && is_array($object->$k) && isset($object->$v) && method_exists($object->$v, 'getType') && $object->$v->getType() == 'hasAndBelongsToMany'){
 824                  $object->$v->load();
 825                  $success = $object->$v->deleteAll() ? $success : false;
 826              }
 827          }
 828  
 829          return $success;
 830      }
 831  
 832      function _afterCallback(&$object)
 833      {
 834          static $joined_items = array();
 835          $success = true;
 836  
 837          $object_id = $object->getId();
 838          foreach (array_keys($object->hasAndBelongsToMany->models) as $association_id){
 839              $CollectionHandler =& $object->hasAndBelongsToMany->models[$association_id];
 840              $options = $CollectionHandler->getOptions($association_id);
 841              $class_name = strtolower($CollectionHandler->getOption($association_id, 'class_name'));
 842  
 843              if(!empty($object->$association_id) && is_array($object->$association_id)){
 844                  $this->_removeDuplicates($object, $association_id);
 845                  foreach (array_keys($object->$association_id) as $k){
 846  
 847                      if(!empty($object->{$association_id}[$k]) && strtolower(get_class($object->{$association_id}[$k])) == $class_name){
 848                          $AssociatedItem =& $object->{$association_id}[$k];
 849                          // This helps avoiding double realation on first time savings
 850  
 851  
 852                          if(!in_array($AssociatedItem->__hasAndBelongsToManyMemberId, $joined_items)){
 853                              $joined_items[] = $AssociatedItem->__hasAndBelongsToManyMemberId;
 854                              
 855                              if(empty($AssociatedItem->hasAndBelongsToMany->__joined) && ($AssociatedItem->isNewRecord()? $AssociatedItem->save() : true)){
 856                                  $AssociatedItem->hasAndBelongsToMany->__joined = true;
 857                                  $CollectionHandler->JoinObject =& $CollectionHandler->JoinObject->create(array($options['foreign_key'] => $object_id ,$options['association_foreign_key'] => $AssociatedItem->getId()));
 858  
 859  
 860                                  $success = !$CollectionHandler->JoinObject->isNewRecord() ? $success : false;
 861  
 862                              }else{
 863                                  $success = false;
 864                              }
 865                          }
 866                      }
 867                  }
 868              }
 869              if(!$success){
 870                  return $success;
 871              }
 872          }
 873          return $success;
 874      }
 875  
 876      function _removeDuplicates(&$object, $association_id)
 877      {
 878          if(!empty($object->{$association_id})){
 879              $CollectionHandler =& $object->hasAndBelongsToMany->models[$association_id];
 880              $options = $CollectionHandler->getOptions($association_id);
 881              if(empty($options['unique'])){
 882                  return ;
 883              }
 884              if($object->isNewRecord()){
 885                  $ids = array();
 886              }else{
 887                  if($existing = $CollectionHandler->find()){
 888                      $ids = $existing[0]->collect($existing,'id','id');
 889                  }else{
 890                      $ids = array();
 891                  }
 892              }
 893              $class_name = strtolower($CollectionHandler->getOption($association_id, 'class_name'));
 894              foreach (array_keys($object->$association_id) as $k){
 895                  if(!empty($object->{$association_id}[$k]) && strtolower(get_class($object->{$association_id}[$k])) == $class_name && !$object->{$association_id}[$k]->isNewRecord()){
 896                      $AssociatedItem =& $object->{$association_id}[$k];
 897                      if(isset($ids[$AssociatedItem->getId()])){
 898                          unset($object->{$association_id}[$k]);
 899                          continue;
 900                      }
 901                      $ids[$AssociatedItem->getId()] = true;
 902                  }
 903              }
 904          }
 905      }
 906  }
 907  
 908  
 909  ?>


Generated: Mon Oct 27 12:43:49 2008 Cross-referenced by PHPXref 0.6