Webylon 3.1 API Docs
  • Package
  • Class
  • Tree
  • Deprecated
  • Download
Version: current
  • 3.2
  • 3.1

Packages

  • auth
  • Booking
  • cart
    • shipping
    • steppedcheckout
  • Catalog
  • cms
    • assets
    • batchaction
    • batchactions
    • bulkloading
    • comments
    • content
    • core
    • export
    • newsletter
    • publishers
    • reports
    • security
    • tasks
  • Dashboard
  • DataObjectManager
  • event
  • faq
  • forms
    • actions
    • core
    • fields-basic
    • fields-dataless
    • fields-datetime
    • fields-files
    • fields-formatted
    • fields-formattedinput
    • fields-relational
    • fields-structural
    • transformations
    • validators
  • googlesitemaps
  • guestbook
  • installer
  • newsletter
  • None
  • photo
    • gallery
  • PHP
  • polls
  • recaptcha
  • sapphire
    • api
    • bulkloading
    • control
    • core
    • cron
    • dev
    • email
    • fields-formattedinput
    • filesystem
    • formatters
    • forms
    • i18n
    • integration
    • misc
    • model
    • parsers
    • search
    • security
    • tasks
    • testing
    • tools
    • validation
    • view
    • widgets
  • seo
    • open
      • graph
  • sfDateTimePlugin
  • spamprotection
  • stealth
    • captha
  • subsites
  • userform
    • pagetypes
  • userforms
  • webylon
  • widgets

Classes

  • AdditionalMenuWidget_Item
  • AdvancedSliderHomepageWidget_Item
  • AssetManagerFolder
  • BannerWidget_Item
  • BaseObjectDecorator
  • BookingOrder
  • BookingPaymentMethod
  • BookingService
  • Boolean
  • ButtonsBlockHomepageWidget_Item
  • CarouselHomepageWidget_Item
  • CatalogRubricsHomepageWidget_CatalogDecorator
  • ClientEmailOrderNotification
  • ClientVKOrderNotification
  • ComponentSet
  • Currency
  • DatabaseAdmin
  • DataObject
  • DataObjectDecorator
  • DataObjectLog
  • DataObjectSet
  • DataObjectSet_Iterator
  • Date
  • DB
  • DBField
  • Decimal
  • DocumentItem
  • DocumentPage_File
  • Double
  • Enum
  • ErrorPageSubsite
  • FileDataObjectTrackingDecorator
  • FileImportDecorator
  • Float
  • ForeignKey
  • Hierarchy
  • HTMLText
  • HTMLVarchar
  • ImportLog_Item
  • Int
  • ManagerEmailOrderNotification
  • Material3D_File
  • MediawebPage_File
  • MediawebPage_Photo
  • MobileContentDecorator
  • Money
  • MultiEnum
  • MySQLDatabase
  • MySQLQuery
  • OrderDataObject
  • OrderHandlersDecorator
  • OrderItemVariationDecorator
  • OrderService
  • OrderServiceOrder
  • OrdersExportDecorator
  • PageIcon
  • PageWidgets
  • Payment
  • PaymentMethodShippingDecorator
  • PaymentOrderExtension
  • Percentage
  • PhotoAlbumItem
  • PhotoAlbumProductLinkDecorator
  • PhotoAlbumWidgetLinkDecorator
  • PhotoGalleryHomepageWidget_Item
  • PrimaryKey
  • Product3DDecorator
  • ProductCatalogCatalogLinkedDecorator
  • RatePeriod
  • RealtyImportLog
  • RealtyImportLog_Item
  • RedirectEntry
  • RoomOrder
  • RoomOrderPerson
  • RoomRate
  • RoomService
  • RoomServiceOrder
  • SberbankPaymentDecorator
  • SeoOpenGraphPageDecorator
  • ServiceOrder
  • ShippingMethodPaymentDecorator
  • ShopCountry
  • SimpleOrderCatalogDecorator
  • SimpleOrderProductDecorator
  • SiteConfigWidgets
  • SiteTreeDecorator
  • SiteTreeImportDecorator
  • SliderHomepageWidget_Item
  • SMSCOrderNotification
  • SMSOrderNotification
  • SortableDataObject
  • SQLMap
  • SQLMap_Iterator
  • SQLQuery
  • SS_Database
  • SS_Datetime
  • SS_Query
  • StringField
  • SubsiteDomain
  • Text
  • TextAnonsWidget_Item
  • Texture3D_File
  • Time
  • Varchar
  • Versioned
  • Versioned_Version
  • VideoCategory
  • VideoEntry
  • VKNotificationQueue
  • WebylonWidget_Item
  • YaMoneyPaymentDecorator
  • Year

Interfaces

  • CompositeDBField
  • CurrentPageIdentifier
  • DataObjectInterface
   1 <?php
   2 /**
   3  * A single database record & abstract class for the data-access-model.
   4  *
   5  * <h2>Extensions and Decorators</h2>
   6  *
   7  * See {@link Extension} and {@link DataObjectDecorator}.
   8  * 
   9  * <h2>Permission Control</h2>
  10  * 
  11  * Object-level access control by {@link Permission}. Permission codes are arbitrary
  12  * strings which can be selected on a group-by-group basis.
  13  * 
  14  * <code>
  15  * class Article extends DataObject implements PermissionProvider {
  16  *  static $api_access = true;
  17  *  
  18  *  public function canView($member = false) {
  19  *      return Permission::check('ARTICLE_VIEW');
  20  *  }
  21  *  public function canEdit($member = false) {
  22  *      return Permission::check('ARTICLE_EDIT');
  23  *  }
  24  *  public function canDelete() {
  25  *      return Permission::check('ARTICLE_DELETE');
  26  *  }
  27  *  public function canCreate() {
  28  *      return Permission::check('ARTICLE_CREATE');
  29  *  }
  30  *  public function providePermissions() {
  31  *      return array(
  32  *          'ARTICLE_VIEW' => 'Read an article object',
  33  *          'ARTICLE_EDIT' => 'Edit an article object',
  34  *          'ARTICLE_DELETE' => 'Delete an article object',
  35  *          'ARTICLE_CREATE' => 'Create an article object',
  36  *      );
  37  *  }
  38  * }
  39  * </code> 
  40  *
  41  * Object-level access control by {@link Group} membership: 
  42  * <code>
  43  * class Article extends DataObject {
  44  *  static $api_access = true;
  45  *  
  46  *  public function canView($member = false) {
  47  *      if(!$member) $member = Member::currentUser();
  48  *      return $member->inGroup('Subscribers');
  49  *  }
  50  *  public function canEdit($member = false) {
  51  *      if(!$member) $member = Member::currentUser();
  52  *      return $member->inGroup('Editors');
  53  *  }
  54  *  
  55  *  // ...
  56  * }
  57  * </code>
  58  * 
  59  * If any public method on this class is prefixed with an underscore, 
  60  * the results are cached in memory through {@link cachedCall()}.
  61  * 
  62  * 
  63  * @todo Add instance specific removeExtension() which undos loadExtraStatics()
  64  *  and defineMethods()
  65  * 
  66  * @package sapphire
  67  * @subpackage model
  68  */
  69 class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider {
  70     
  71     /**
  72      * Human-readable singular name.
  73      * @var string
  74      */
  75     public static $singular_name = null;
  76     
  77     /**
  78      * Human-readable pluaral name
  79      * @var string
  80      */
  81     public static $plural_name = null;
  82     
  83     /**
  84      * Allow API access to this object?
  85      * @todo Define the options that can be set here
  86      */
  87     public static $api_access = false;
  88     
  89     public static
  90         $cache_has_own_table       = array(),
  91         $cache_has_own_table_field = array();
  92     
  93     /**
  94      * True if this DataObject has been destroyed.
  95      * @var boolean
  96      */
  97     public $destroyed = false;
  98     
  99     /**
 100      * Data stored in this objects database record. An array indexed
 101      * by fieldname.
 102      * @var array
 103      */
 104     protected $record;
 105 
 106     /**
 107      * An array indexed by fieldname, true if the field has been changed.
 108      * Use {@link getChangedFields()} and {@link isChanged()} to inspect
 109      * the changed state.
 110      * 
 111      * @var array
 112      */
 113     private $changed;
 114 
 115     /**
 116      * The database record (in the same format as $record), before
 117      * any changes.
 118      * @var array
 119      */
 120     protected $original;
 121 
 122     /**
 123      * The one-to-one, one-to-many and many-to-one components
 124      * indexed by component name.
 125      * @var array
 126      */
 127     protected $components;
 128     
 129     /**
 130      * Used by onBeforeDelete() to ensure child classes call parent::onBeforeDelete()
 131      * @var boolean
 132      */
 133     protected $brokenOnDelete = false;
 134     
 135     /**
 136      * Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite()
 137      * @var boolean
 138      */
 139     protected $brokenOnWrite = false;
 140     
 141     /**
 142      * Should dataobjects be validated before they are written?
 143      */
 144     private static $validation_enabled = true;
 145     
 146     /**
 147      * Returns when validation on DataObjects is enabled.
 148      * @return bool
 149      */
 150     static function get_validation_enabled() {
 151         return self::$validation_enabled;
 152     }
 153     
 154     /**
 155      * Set whether DataObjects should be validated before they are written.
 156      * @param $enable bool
 157      * @see DataObject::validate()
 158      */
 159     static function set_validation_enabled($enable) {
 160         self::$validation_enabled = (bool) $enable;
 161     }
 162 
 163     /**
 164      * Return the complete map of fields on this object, including Created, LastEdited and ClassName
 165      *
 166      * @param string $class
 167      * @return array
 168      */
 169     public static function database_fields($class) {
 170         if(get_parent_class($class) == 'DataObject') {
 171             return array_merge (
 172                 array (
 173                     'ClassName'  => "Enum('" . implode(', ', ClassInfo::subclassesFor($class)) . "')",
 174                     'Created'    => 'SS_Datetime',
 175                     'LastEdited' => 'SS_Datetime'
 176                 ),
 177                 self::custom_database_fields($class)
 178             );
 179         }
 180 
 181         return self::custom_database_fields($class);
 182     }
 183 
 184     /**
 185      * Get all database columns explicitly defined on a class in {@link DataObject::$db} 
 186      * and {@link DataObject::$has_one}. Resolves instances of {@link CompositeDBField} 
 187      * into the actual database fields, rather than the name of the field which 
 188      * might not equate a database column.
 189      * 
 190      * @uses CompositeDBField->compositeDatabaseFields()
 191      *
 192      * @param string $class
 193      * @return array Map of fieldname to specification, similiar to {@link DataObject::$db}.
 194      */
 195     public static function custom_database_fields($class) {
 196         $fields = Object::uninherited_static($class, 'db');
 197         
 198         foreach(self::composite_fields($class, false) as $fieldName => $fieldClass) {
 199             // Remove the original fieldname, its not an actual database column
 200             unset($fields[$fieldName]);
 201             
 202             // Add all composite columns
 203             $compositeFields = singleton($fieldClass)->compositeDatabaseFields();
 204             if($compositeFields) foreach($compositeFields as $compositeName => $spec) {
 205                 $fields["{$fieldName}{$compositeName}"] = $spec;
 206             }
 207         }
 208         
 209         // Add has_one relationships
 210         $hasOne = Object::uninherited_static($class, 'has_one');
 211         if($hasOne) foreach(array_keys($hasOne) as $field) {
 212             $fields[$field . 'ID'] = 'ForeignKey';
 213         }
 214         
 215         return (array)$fields;
 216     }
 217     
 218     private static $_cache_custom_database_fields = array();
 219     
 220     
 221     /**
 222      * Returns the field class if the given db field on the class is a composite field.
 223      * Will check all applicable ancestor classes and aggregate results.
 224      */
 225     static function is_composite_field($class, $name, $aggregated = true) {
 226         if(!isset(self::$_cache_composite_fields[$class])) self::cache_composite_fields($class);
 227         
 228         if(isset(self::$_cache_composite_fields[$class][$name])) {
 229             return self::$_cache_composite_fields[$class][$name];
 230             
 231         } else if($aggregated && $class != 'DataObject' && ($parentClass=get_parent_class($class)) != 'DataObject') {
 232             return self::is_composite_field($parentClass, $name);
 233         }
 234     }
 235 
 236     /**
 237      * Returns a list of all the composite if the given db field on the class is a composite field.
 238      * Will check all applicable ancestor classes and aggregate results.
 239      */
 240     static function composite_fields($class, $aggregated = true) {
 241         if(!isset(self::$_cache_composite_fields[$class])) self::cache_composite_fields($class);
 242         
 243         $compositeFields = self::$_cache_composite_fields[$class];
 244         
 245         if($aggregated && $class != 'DataObject' && ($parentClass=get_parent_class($class)) != 'DataObject') {
 246             $compositeFields = array_merge($compositeFields, 
 247                 self::composite_fields($parentClass));
 248         }
 249         
 250         return $compositeFields;
 251     }
 252 
 253     /**
 254      * Internal cacher for the composite field information
 255      */
 256     private static function cache_composite_fields($class) {
 257         $compositeFields = array();
 258         
 259         $fields = Object::uninherited_static($class, 'db');
 260         if($fields) foreach($fields as $fieldName => $fieldClass) {
 261             // Strip off any parameters
 262             $bPos = strpos('(', $fieldClass);
 263             if($bPos !== FALSE) $fieldClass = substr(0,$bPos, $fieldClass);
 264             
 265             // Test to see if it implements CompositeDBField
 266             if(ClassInfo::classImplements($fieldClass, 'CompositeDBField')) {
 267                 $compositeFields[$fieldName] = $fieldClass;
 268             }
 269         }
 270         
 271         self::$_cache_composite_fields[$class] = $compositeFields;
 272     }
 273     
 274     private static $_cache_composite_fields = array();
 275     
 276 
 277     /**
 278      * This controls whether of not extendCMSFields() is called by getCMSFields.
 279      */
 280     private static $runCMSFieldsExtensions = true;
 281 
 282     /**
 283      * Stops extendCMSFields() being called on getCMSFields().
 284      * This is useful when you need access to fields added by subclasses
 285      * of SiteTree in a decorator. Call before calling parent::getCMSFields(),
 286      * and reenable afterwards.
 287      */
 288     public static function disableCMSFieldsExtensions() {
 289         self::$runCMSFieldsExtensions = false;
 290     }
 291     
 292     /**
 293      * Reenables extendCMSFields() being called on getCMSFields() after
 294      * it has been disabled by disableCMSFieldsExtensions().
 295      */
 296     public static function enableCMSFieldsExtensions() {
 297         self::$runCMSFieldsExtensions = true;
 298     }
 299 
 300     /**
 301      * Construct a new DataObject.
 302      *
 303      * @param array|null $record This will be null for a new database record.  Alternatively, you can pass an array of
 304      * field values.  Normally this contructor is only used by the internal systems that get objects from the database.
 305      * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.  Singletons
 306      * don't have their defaults set.
 307      */
 308     function __construct($record = null, $isSingleton = false) {
 309         // Set the fields data.
 310         if(!$record) {
 311             $record = array(
 312                 'ID' => 0,
 313                 'ClassName' => get_class($this),
 314                 'RecordClassName' => get_class($this)
 315             );
 316         }
 317 
 318         if(!is_array($record)) {
 319             if(is_object($record)) $passed = "an object of type '$record->class'";
 320             else $passed = "The value '$record'";
 321 
 322             user_error("DataObject::__construct passed $passed.  It's supposed to be passed an array,
 323             taken straight from the database.  Perhaps you should use DataObject::get_one instead?", E_USER_WARNING);
 324             $record = null;
 325         }
 326 
 327         // Convert PostgreSQL boolean values
 328         // TODO: Implement this more elegantly, for example by writing a more intelligent SQL SELECT query prior to object construction
 329         if(DB::getConn() instanceof PostgreSQLDatabase) {
 330             $this->class = get_class($this);
 331             foreach($record as $k => $v) {
 332                 if($this->db($k) == 'Boolean' && $v == 'f') $record[$k] = '0';
 333             }
 334         }
 335         
 336         // TODO: MSSQL has a convert function that can do this on the SQL end. We just need a
 337         // nice way of telling the database how we want to get the value out on a per-fieldtype basis
 338         if(DB::getConn() instanceof MSSQLDatabase) {
 339             $this->class = get_class($this);
 340             foreach($record as $k => $v) {
 341                 if($v) {
 342                     if($k == 'Created' || $k == 'LastEdited') {
 343                         $fieldtype = 'SS_Datetime';
 344                     } else {
 345                         $fieldtype = $this->db($k);
 346                     }
 347                 
 348                     // MSSQLDatabase::date() uses datetime for the data type for "Date" and "SS_Datetime"
 349                     switch($fieldtype) {
 350                         case "Date":
 351                             $v = preg_replace('/:[0-9][0-9][0-9]([ap]m)$/i', ' \\1', $v);
 352                             $record[$k] = date('Y-m-d', strtotime($v));
 353                             break;
 354                     
 355                         case "Datetime":
 356                         case "SS_Datetime":
 357                             $v = preg_replace('/:[0-9][0-9][0-9]([ap]m)$/i', ' \\1', $v);
 358                             $record[$k] = date('Y-m-d H:i:s', strtotime($v));
 359                             break;
 360                     }
 361                 }
 362             }
 363         }
 364 
 365         // Set $this->record to $record, but ignore NULLs
 366         $this->record = array();
 367         foreach($record as $k => $v) {
 368             // Ensure that ID is stored as a number and not a string
 369             // To do: this kind of clean-up should be done on all numeric fields, in some relatively
 370             // performant manner
 371             if($v !== null) {
 372                 if($k == 'ID' && is_numeric($v)) $this->record[$k] = (int)$v;
 373                 else $this->record[$k] = $v;
 374             }
 375         }
 376         $this->original = $this->record;
 377 
 378         // Keep track of the modification date of all the data sourced to make this page
 379         // From this we create a Last-Modified HTTP header
 380         if(isset($record['LastEdited'])) {
 381             HTTP::register_modification_date($record['LastEdited']);
 382         }
 383 
 384         parent::__construct();
 385 
 386         // Must be called after parent constructor
 387         if(!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
 388             $this->populateDefaults();
 389         }
 390 
 391         // prevent populateDefaults() and setField() from marking overwritten defaults as changed
 392         $this->changed = array();
 393     }
 394 
 395     /**
 396      * Destroy all of this objects dependant objects.
 397      * You'll need to call this to get the memory of an object that has components or extensions freed.
 398      */
 399     function destroy() {
 400         $this->extension_instances = null;
 401         $this->components = null;
 402         $this->destroyed = true;
 403         $this->record = null;
 404         $this->original = null;
 405         $this->changed = null;
 406         $this->flushCache(false);
 407     }
 408 
 409     /**
 410      * Create a duplicate of this node.
 411      * Caution: Doesn't duplicate relations.
 412      *
 413      * @param $doWrite Perform a write() operation before returning the object.  If this is true, it will create the duplicate in the database.
 414      * @return DataObject A duplicate of this node. The exact type will be the type of this node.
 415      */
 416     function duplicate($doWrite = true) {
 417         $className = $this->class;
 418         $clone = new $className( $this->record );
 419         $clone->ID = 0;
 420         if($doWrite) $clone->write();
 421         return $clone;
 422     }
 423 
 424     /**
 425      * Set the ClassName attribute. {@link $class} is also updated.
 426      * Warning: This will produce an inconsistent record, as the object
 427      * instance will not automatically switch to the new subclass.
 428      * Please use {@link newClassInstance()} for this purpose,
 429      * or destroy and reinstanciate the record.
 430      *
 431      * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
 432      */
 433     function setClassName($className) {
 434         $className = trim($className);
 435         if(!$className || !is_subclass_of($className, 'DataObject')) return;
 436 
 437         $this->class = $className;
 438         $this->setField("ClassName", $className);
 439     }
 440 
 441     /**
 442      * Create a new instance of a different class from this object's record.
 443      * This is useful when dynamically changing the type of an instance. Specifically,
 444      * it ensures that the instance of the class is a match for the className of the
 445      * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
 446      * property manually before calling this method, as it will confuse change detection.
 447      * 
 448      * If the new class is different to the original class, defaults are populated again
 449      * because this will only occur automatically on instantiation of a DataObject if
 450      * there is no record, or the record has no ID. In this case, we do have an ID but
 451      * we still need to repopulate the defaults.
 452      *
 453      * @param string $newClassName The name of the new class
 454      *
 455      * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
 456      */
 457     function newClassInstance($newClassName) {
 458         $originalClass = $this->ClassName;
 459         $newInstance = new $newClassName(array_merge(
 460             $this->record,
 461             array(
 462                 'ClassName' => $originalClass,
 463                 'RecordClassName' => $originalClass,
 464             )
 465         ));
 466         
 467         if($newClassName != $originalClass) {
 468             $newInstance->setClassName($newClassName);
 469             $newInstance->populateDefaults();
 470             $newInstance->forceChange();
 471         }
 472 
 473         return $newInstance;
 474     }
 475 
 476     /**
 477      * Adds methods from the extensions.
 478      * Called by Object::__construct() once per class.
 479      */
 480     function defineMethods() {
 481         parent::defineMethods();
 482 
 483         // Define the extra db fields - this is only necessary for extensions added in the
 484         // class definition.  Object::add_extension() will call this at definition time for
 485         // those objects, which is a better mechanism.  Perhaps extensions defined inside the
 486         // class def can somehow be applied at definiton time also?
 487         if($this->extension_instances) foreach($this->extension_instances as $i => $instance) {
 488             if(!$instance->class) {
 489                 $class = get_class($instance);
 490                 user_error("DataObject::defineMethods(): Please ensure {$class}::__construct() calls parent::__construct()", E_USER_ERROR);
 491             }
 492         }
 493 
 494         if($this->class == 'DataObject') return;
 495 
 496         // Set up accessors for joined items
 497         if($manyMany = $this->many_many()) {
 498             foreach($manyMany as $relationship => $class) {
 499                 $this->addWrapperMethod($relationship, 'getManyManyComponents');
 500             }
 501         }
 502         if($hasMany = $this->has_many()) {
 503 
 504             foreach($hasMany as $relationship => $class) {
 505                 $this->addWrapperMethod($relationship, 'getComponents');
 506             }
 507 
 508         }
 509         if($hasOne = $this->has_one()) {
 510             foreach($hasOne as $relationship => $class) {
 511                 $this->addWrapperMethod($relationship, 'getComponent');
 512             }
 513         }
 514         if($belongsTo = $this->belongs_to()) foreach(array_keys($belongsTo) as $relationship) {
 515             $this->addWrapperMethod($relationship, 'getComponent');
 516         }
 517     }
 518 
 519     /**
 520      * Returns true if this object "exists", i.e., has a sensible value.
 521      * The default behaviour for a DataObject is to return true if
 522      * the object exists in the database, you can override this in subclasses.
 523      *
 524      * @return boolean true if this object exists
 525      */
 526     public function exists() {
 527         return ($this->record && $this->record['ID'] > 0);
 528     }
 529 
 530     public function isEmpty(){
 531         $isEmpty = true;
 532         if($this->record){
 533             foreach($this->record as $k=>$v){
 534                 if($k != "ID"){
 535                     $isEmpty = $isEmpty && !$v;
 536                 }
 537             }
 538         }
 539         return $isEmpty;
 540     }
 541 
 542     /**
 543      * Get the user friendly singular name of this DataObject.
 544      * If the name is not defined (by redefining $singular_name in the subclass),
 545      * this returns the class name.
 546      *
 547      * @return string User friendly singular name of this DataObject
 548      */
 549     function singular_name() {
 550         if(!$name = $this->stat('singular_name')) {
 551             $name = ucwords(trim(strtolower(preg_replace('/_?([A-Z])/', ' $1', $this->class))));
 552         }
 553         
 554         return $name;
 555     }
 556 
 557     /**
 558      * Get the translated user friendly singular name of this DataObject
 559      * same as singular_name() but runs it through the translating function
 560      *
 561      * Translating string is in the form:
 562      *     $this->class.SINGULARNAME
 563      * Example:
 564      *     Page.SINGULARNAME
 565      *
 566      * @return string User friendly translated singular name of this DataObject
 567      */
 568     function i18n_singular_name() {
 569         return _t($this->class.'.SINGULARNAME', $this->singular_name());
 570     }
 571 
 572     /**
 573      * Get the user friendly plural name of this DataObject
 574      * If the name is not defined (by renaming $plural_name in the subclass),
 575      * this returns a pluralised version of the class name.
 576      *
 577      * @return string User friendly plural name of this DataObject
 578      */
 579     function plural_name() {
 580         if($name = $this->stat('plural_name')) {
 581             return $name;
 582         } else {
 583             $name = $this->singular_name();
 584             if(substr($name,-1) == 'e') $name = substr($name,0,-1);
 585             else if(substr($name,-1) == 'y') $name = substr($name,0,-1) . 'ie';
 586 
 587             return ucfirst($name . 's');
 588         }
 589     }
 590 
 591     /**
 592      * Get the translated user friendly plural name of this DataObject
 593      * Same as plural_name but runs it through the translation function
 594      * Translation string is in the form:
 595      *      $this->class.PLURALNAME
 596      * Example:
 597      *      Page.PLURALNAME
 598      *
 599      * @return string User friendly translated plural name of this DataObject
 600      */
 601     function i18n_plural_name()
 602     {
 603         $name = $this->plural_name();
 604         return _t($this->class.'.PLURALNAME', $name);
 605     }
 606     
 607     /**
 608      * Standard implementation of a title/label for a specific
 609      * record. Tries to find properties 'Title' or 'Name',
 610      * and falls back to the 'ID'. Useful to provide
 611      * user-friendly identification of a record, e.g. in errormessages
 612      * or UI-selections.
 613      * 
 614      * Overload this method to have a more specialized implementation,
 615      * e.g. for an Address record this could be:
 616      * <code>
 617      * public function getTitle() {
 618      *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
 619      * }
 620      * </code>
 621      *
 622      * @return string
 623      */
 624     public function getTitle() {
 625         if($this->hasDatabaseField('Title')) return $this->getField('Title');
 626         if($this->hasDatabaseField('Name')) return $this->getField('Name');
 627         
 628         return "#{$this->ID}";
 629     }
 630 
 631     /**
 632      * Returns the associated database record - in this case, the object itself.
 633      * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
 634      *
 635      * @return DataObject Associated database record
 636      */
 637     public function data() {
 638         return $this;
 639     }
 640 
 641     /**
 642      * Convert this object to a map.
 643      *
 644      * @return array The data as a map.
 645      */
 646     public function toMap() {
 647         return $this->record;
 648     }
 649 
 650     /**
 651      * Update a number of fields on this object, given a map of the desired changes.
 652      * 
 653      * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
 654      * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
 655      * 
 656      * update() doesn't write the main object, but if you use the dot syntax, it will write() 
 657      * the related objects that it alters.
 658      *
 659      * @param array $data A map of field name to data values to update.
 660      */
 661     public function update($data) {
 662         foreach($data as $k => $v) {
 663             // Implement dot syntax for updates
 664             if(strpos($k,'.') !== false) {
 665                 $relations = explode('.', $k);
 666                 $fieldName = array_pop($relations);
 667                 $relObj = $this;
 668                 foreach($relations as $i=>$relation) {
 669                     // no support for has_many or many_many relationships,
 670                     // as the updater wouldn't know which object to write to (or create)
 671                     if($relObj->$relation() instanceof DataObject) {
 672                         $relObj = $relObj->$relation();
 673                         
 674                         // If the intermediate relationship objects have been created, then write them
 675                         if($i<sizeof($relation)-1 && !$relObj->ID) $relObj->write();
 676                     } else {
 677                         user_error(
 678                             "DataObject::update(): Can't traverse relationship '$relation'," .  
 679                             "it has to be a has_one relationship or return a single DataObject", 
 680                             E_USER_NOTICE
 681                         );
 682                         // unset relation object so we don't write properties to the wrong object
 683                         unset($relObj);
 684                         break;
 685                     }
 686                 }
 687 
 688                 if($relObj) {
 689                     $relObj->$fieldName = $v;
 690                     $relObj->write();
 691                     $relObj->flushCache();
 692                 } else {
 693                     user_error("Couldn't follow dot syntax '$k' on '$this->class' object", E_USER_WARNING);
 694                 }
 695             } else {
 696                 $this->$k = $v;
 697             }
 698         }
 699     }
 700     
 701     /**
 702      * Pass changes as a map, and try to
 703      * get automatic casting for these fields.
 704      * Doesn't write to the database. To write the data,
 705      * use the write() method.
 706      *
 707      * @param array $data A map of field name to data values to update.
 708      */
 709     public function castedUpdate($data) {
 710         foreach($data as $k => $v) {
 711             $this->setCastedField($k,$v);
 712         }
 713     }
 714 
 715     /**
 716      * Merges data and relations from another object of same class,
 717      * without conflict resolution. Allows to specify which
 718      * dataset takes priority in case its not empty.
 719      * has_one-relations are just transferred with priority 'right'.
 720      * has_many and many_many-relations are added regardless of priority.
 721      *
 722      * Caution: has_many/many_many relations are moved rather than duplicated,
 723      * meaning they are not connected to the merged object any longer.
 724      * Caution: Just saves updated has_many/many_many relations to the database,
 725      * doesn't write the updated object itself (just writes the object-properties).
 726      * Caution: Does not delete the merged object.
 727      * Caution: Does now overwrite Created date on the original object.
 728      *
 729      * @param $obj DataObject
 730      * @param $priority String left|right Determines who wins in case of a conflict (optional)
 731      * @param $includeRelations Boolean Merge any existing relations (optional)
 732      * @param $overwriteWithEmpty Boolean Overwrite existing left values with empty right values.
 733      *  Only applicable with $priority='right'. (optional)
 734      * @return Boolean
 735      */
 736     public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false) {
 737         $leftObj = $this;
 738 
 739         if($leftObj->ClassName != $rightObj->ClassName) {
 740             // we can't merge similiar subclasses because they might have additional relations
 741             user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
 742             (expected '{$leftObj->ClassName}').", E_USER_WARNING);
 743             return false;
 744         }
 745 
 746         if(!$rightObj->ID) {
 747             user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
 748                 to make sure all relations are transferred properly.').", E_USER_WARNING);
 749             return false;
 750         }
 751 
 752         // makes sure we don't merge data like ID or ClassName
 753         $leftData = $leftObj->inheritedDatabaseFields();
 754         $rightData = $rightObj->inheritedDatabaseFields();
 755 
 756         foreach($rightData as $key=>$rightVal) {
 757             // don't merge conflicting values if priority is 'left'
 758             if($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) continue;
 759 
 760             // don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
 761             if($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) continue;
 762 
 763             // TODO remove redundant merge of has_one fields
 764             $leftObj->{$key} = $rightObj->{$key};
 765         }
 766 
 767         // merge relations
 768         if($includeRelations) {
 769             if($manyMany = $this->many_many()) {
 770                 foreach($manyMany as $relationship => $class) {
 771                     $leftComponents = $leftObj->getManyManyComponents($relationship);
 772                     $rightComponents = $rightObj->getManyManyComponents($relationship);
 773                     if($rightComponents && $rightComponents->exists()) $leftComponents->addMany($rightComponents->column('ID'));
 774                     $leftComponents->write();
 775                 }
 776             }
 777 
 778             if($hasMany = $this->has_many()) {
 779                 foreach($hasMany as $relationship => $class) {
 780                     $leftComponents = $leftObj->getComponents($relationship);
 781                     $rightComponents = $rightObj->getComponents($relationship);
 782                     if($rightComponents && $rightComponents->exists()) $leftComponents->addMany($rightComponents->column('ID'));
 783                     $leftComponents->write();
 784                 }
 785 
 786             }
 787 
 788             if($hasOne = $this->has_one()) {
 789                 foreach($hasOne as $relationship => $class) {
 790                     $leftComponent = $leftObj->getComponent($relationship);
 791                     $rightComponent = $rightObj->getComponent($relationship);
 792                     if($leftComponent->exists() && $rightComponent->exists() && $priority == 'right') {
 793                         $leftObj->{$relationship . 'ID'} = $rightObj->{$relationship . 'ID'};
 794                     }
 795                 }
 796             }
 797         }
 798 
 799         return true;
 800     }
 801 
 802     /**
 803      * Forces the record to think that all its data has changed.
 804      * Doesn't write to the database. Only sets fields as changed
 805      * if they are not already marked as changed.
 806      */
 807     public function forceChange() {
 808         // $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
 809         $fieldNames = array_unique(array_merge(array_keys($this->record), array_keys($this->inheritedDatabaseFields())));
 810         
 811         foreach($fieldNames as $fieldName) {
 812             if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = 1;
 813             // Populate the null values in record so that they actually get written
 814             if(!isset($this->record[$fieldName])) $this->record[$fieldName] = null;
 815         }
 816         
 817         // @todo Find better way to allow versioned to write a new version after forceChange
 818         if($this->isChanged('Version')) unset($this->changed['Version']);
 819     }
 820     
 821     /**
 822      * Validate the current object.
 823      *
 824      * By default, there is no validation - objects are always valid!  However, you can overload this method in your
 825      * DataObject sub-classes to specify custom validation.
 826      * 
 827      * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
 828      * and onAfterWrite() won't get called either.
 829      * 
 830      * It is expected that you call validate() in your own application to test that an object is valid before attempting
 831      * a write, and respond appropriately if it isnt'.
 832      * 
 833      * @return A {@link ValidationResult} object
 834      */
 835     protected function validate() {
 836         return new ValidationResult();
 837     }
 838 
 839     /**
 840      * Event handler called before writing to the database.
 841      * You can overload this to clean up or otherwise process data before writing it to the
 842      * database.  Don't forget to call parent::onBeforeWrite(), though!
 843      *
 844      * This called after {@link $this->validate()}, so you can be sure that your data is valid.
 845      * 
 846      * @uses DataObjectDecorator->onBeforeWrite()
 847      */
 848     protected function onBeforeWrite() {
 849         $this->brokenOnWrite = false;
 850         
 851         $dummy = null;
 852         $this->extend('onBeforeWrite', $dummy);
 853     }
 854 
 855     /**
 856      * Event handler called after writing to the database.
 857      * You can overload this to act upon changes made to the data after it is written.
 858      * $this->changed will have a record
 859      * database.  Don't forget to call parent::onAfterWrite(), though!
 860      *
 861      * @uses DataObjectDecorator->onAfterWrite()
 862      */
 863     protected function onAfterWrite() {
 864         $dummy = null;
 865         $this->extend('onAfterWrite', $dummy);
 866     }
 867 
 868     /**
 869      * Event handler called before deleting from the database.
 870      * You can overload this to clean up or otherwise process data before delete this
 871      * record.  Don't forget to call parent::onBeforeDelete(), though!
 872      *
 873      * @uses DataObjectDecorator->onBeforeDelete()
 874      */
 875     protected function onBeforeDelete() {
 876         $this->brokenOnDelete = false;
 877         
 878         $dummy = null;
 879         $this->extend('onBeforeDelete', $dummy);
 880     }
 881     
 882     protected function onAfterDelete() {
 883         $this->extend('onAfterDelete');
 884     }
 885 
 886     /**
 887      * Load the default values in from the self::$defaults array.
 888      * Will traverse the defaults of the current class and all its parent classes.
 889      * Called by the constructor when creating new records.
 890      * 
 891      *  @uses DataObjectDecorator->populateDefaults()
 892      */
 893     public function populateDefaults() {
 894         $classes = array_reverse(ClassInfo::ancestry($this));
 895         
 896         foreach($classes as $class) {
 897             $defaults = Object::uninherited_static($class, 'defaults');
 898             
 899             if($defaults && !is_array($defaults)) {
 900                 user_error("Bad '$this->class' defaults given: " . var_export($defaults, true),
 901                     E_USER_WARNING);
 902                 $defaults = null;
 903             }
 904             
 905             if($defaults) foreach($defaults as $fieldName => $fieldValue) {
 906                 // SRM 2007-03-06: Stricter check
 907                 if(!isset($this->$fieldName) || $this->$fieldName === null) {
 908                     $this->$fieldName = $fieldValue;
 909                 }
 910                 // Set many-many defaults with an array of ids
 911                 if(is_array($fieldValue) && $this->many_many($fieldName)) {
 912                     $manyManyJoin = $this->$fieldName();
 913                     $manyManyJoin->setByIdList($fieldValue);
 914                 }
 915             }
 916             if($class == 'DataObject') {
 917                 break;
 918             }
 919         }
 920         
 921         $this->extend('populateDefaults');
 922     }
 923 
 924     /**
 925      * Writes all changes to this object to the database.
 926      *  - It will insert a record whenever ID isn't set, otherwise update.
 927      *  - All relevant tables will be updated.
 928      *  - $this->onBeforeWrite() gets called beforehand.
 929      *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
 930      *  - Calls to {@link DataObjectLog} can be used to see everything that's been changed.
 931      * 
 932      *  @uses DataObjectDecorator->augmentWrite()
 933      *
 934      * @param boolean $showDebug Show debugging information
 935      * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
 936      * @param boolean $forceWrite Write to database even if there are no changes
 937      * @param boolean $writeComponents Call write() on all associated component instances which were previously
 938      *                  retrieved through {@link getComponent()}, {@link getComponents()} or {@link getManyManyComponents()}
 939      *                  (Default: false)
 940      *
 941      * @return int The ID of the record
 942      * @throws ValidationException Exception that can be caught and handled by the calling function
 943      */
 944     public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false) {
 945         $firstWrite = false;
 946         $this->brokenOnWrite = true;
 947         $isNewRecord = false;
 948         
 949         if(self::get_validation_enabled()) {
 950             $valid = $this->validate();
 951             if(!$valid->valid()) {
 952                 // Used by DODs to clean up after themselves, eg, Versioned
 953                 $this->extend('onAfterSkippedWrite');
 954                 throw new ValidationException($valid, "Validation error writing a $this->class object: " . $valid->message() . ".  Object not written.", E_USER_WARNING);
 955                 return false;
 956             }
 957         }
 958 
 959         $this->onBeforeWrite();
 960         if($this->brokenOnWrite) {
 961             user_error("$this->class has a broken onBeforeWrite() function.  Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
 962         }
 963 
 964         // New record = everything has changed
 965 
 966         if(($this->ID && is_numeric($this->ID)) && !$forceInsert) {
 967             $dbCommand = 'update';
 968 
 969             // Update the changed array with references to changed obj-fields
 970             foreach($this->record as $k => $v) {
 971                 if(is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
 972                     $this->changed[$k] = true;
 973                 }
 974             }
 975 
 976         } else{
 977             $dbCommand = 'insert';
 978 
 979             $this->changed = array();
 980             foreach($this->record as $k => $v) {
 981                 $this->changed[$k] = 2;
 982             }
 983             
 984             $firstWrite = true;
 985         }
 986 
 987         // No changes made
 988         if($this->changed) {
 989             foreach($this->getClassAncestry() as $ancestor) {
 990                 if(self::has_own_table($ancestor))
 991                 $ancestry[] = $ancestor;
 992             }
 993 
 994             // Look for some changes to make
 995             if(!$forceInsert) unset($this->changed['ID']);
 996 
 997             $hasChanges = false;
 998             foreach($this->changed as $fieldName => $changed) {
 999                 if($changed) {
1000                     $hasChanges = true;
1001                     break;
1002                 }
1003             }
1004 
1005             if($hasChanges || $forceWrite || !$this->record['ID']) {
1006                     
1007                 // New records have their insert into the base data table done first, so that they can pass the
1008                 // generated primary key on to the rest of the manipulation
1009                 $baseTable = $ancestry[0];
1010                 
1011                 if((!isset($this->record['ID']) || !$this->record['ID']) && isset($ancestry[0])) {  
1012 
1013                     DB::query("INSERT INTO \"{$baseTable}\" (\"Created\") VALUES (" . DB::getConn()->now() . ")");
1014                     $this->record['ID'] = DB::getGeneratedID($baseTable);
1015                     $this->changed['ID'] = 2;
1016 
1017                     $isNewRecord = true;
1018                 }
1019 
1020                 // Divvy up field saving into a number of database manipulations
1021                 $manipulation = array();
1022                 if(isset($ancestry) && is_array($ancestry)) {
1023                     foreach($ancestry as $idx => $class) {
1024                         $classSingleton = singleton($class);
1025                         
1026                         foreach($this->record as $fieldName => $fieldValue) {
1027                             if(isset($this->changed[$fieldName]) && $this->changed[$fieldName] && $fieldType = $classSingleton->hasOwnTableDatabaseField($fieldName)) {
1028                                 $fieldObj = $this->dbObject($fieldName);
1029                                 if(!isset($manipulation[$class])) $manipulation[$class] = array();
1030 
1031                                 // if database column doesn't correlate to a DBField instance...
1032                                 if(!$fieldObj) {
1033                                     $fieldObj = DBField::create('Varchar', $this->record[$fieldName], $fieldName);
1034                                 }
1035 
1036                                 // Both CompositeDBFields and regular fields need to be repopulated
1037                                 $fieldObj->setValue($this->record[$fieldName], $this->record);
1038 
1039                                 if($class != $baseTable || $fieldName!='ID')
1040                                     $fieldObj->writeToManipulation($manipulation[$class]);
1041                             }
1042                         }
1043 
1044                         // Add the class name to the base object
1045                         if($idx == 0) {
1046                             $manipulation[$class]['fields']["LastEdited"] = "'".SS_Datetime::now()->Rfc2822()."'";
1047                             if($dbCommand == 'insert') {
1048                                 $manipulation[$class]['fields']["Created"] = "'".SS_Datetime::now()->Rfc2822()."'";
1049                                 //echo "<li>$this->class - " .get_class($this);
1050                                 $manipulation[$class]['fields']["ClassName"] = "'$this->class'";
1051                             }
1052                         }
1053 
1054                         // In cases where there are no fields, this 'stub' will get picked up on
1055                         if(self::has_own_table($class)) {
1056                             $manipulation[$class]['command'] = $dbCommand;
1057                             $manipulation[$class]['id'] = $this->record['ID'];
1058                         } else {
1059                             unset($manipulation[$class]);
1060                         }
1061                     }
1062                 }
1063                 $this->extend('augmentWrite', $manipulation);
1064                 
1065                 // New records have their insert into the base data table done first, so that they can pass the
1066                 // generated ID on to the rest of the manipulation
1067                 if(isset($isNewRecord) && $isNewRecord && isset($manipulation[$baseTable])) {
1068                     $manipulation[$baseTable]['command'] = 'update';
1069                 }
1070                 
1071                 DB::manipulate($manipulation);
1072 
1073                 if(isset($isNewRecord) && $isNewRecord) {
1074                     DataObjectLog::addedObject($this);
1075                 } else {
1076                     DataObjectLog::changedObject($this);
1077                 }
1078                 
1079                 $this->onAfterWrite();
1080 
1081                 $this->changed = null;
1082             } elseif ( $showDebug ) {
1083                 echo "<b>Debug:</b> no changes for DataObject<br />";
1084                 // Used by DODs to clean up after themselves, eg, Versioned
1085                 $this->extend('onAfterSkippedWrite');
1086             }
1087 
1088             // Clears the cache for this object so get_one returns the correct object.
1089             $this->flushCache();
1090 
1091             if(!isset($this->record['Created'])) {
1092                 $this->record['Created'] = SS_Datetime::now()->Rfc2822();
1093             }
1094             $this->record['LastEdited'] = SS_Datetime::now()->Rfc2822();
1095         } else {
1096             // Used by DODs to clean up after themselves, eg, Versioned
1097             $this->extend('onAfterSkippedWrite');
1098         }
1099 
1100         // Write ComponentSets as necessary
1101         if($writeComponents) {
1102             $this->writeComponents(true);
1103         }
1104         return $this->record['ID'];
1105     }
1106 
1107 
1108     /**
1109      * Write the cached components to the database. Cached components could refer to two different instances of the same record.
1110      * 
1111      * @param $recursive Recursively write components
1112      */
1113     public function writeComponents($recursive = false) {
1114         if(!$this->components) return;
1115         
1116         foreach($this->components as $component) {
1117             $component->write(false, false, false, $recursive);
1118         }
1119     }
1120     
1121     /**
1122      * Perform a write without affecting the version table.
1123      * On objects without versioning.
1124      *
1125      * @return int The ID of the record
1126      */
1127     public function writeWithoutVersion() {
1128         $this->changed['Version'] = 1;
1129         if(!isset($this->record['Version'])) {
1130             $this->record['Version'] = -1;
1131         }
1132         return $this->write();
1133     }
1134 
1135     /**
1136      * Delete this data object.
1137      * $this->onBeforeDelete() gets called.
1138      * Note that in Versioned objects, both Stage and Live will be deleted.
1139      *  @uses DataObjectDecorator->augmentSQL()
1140      */
1141     public function delete() {
1142         $this->brokenOnDelete = true;
1143         $this->onBeforeDelete();
1144         if($this->brokenOnDelete) {
1145             user_error("$this->class has a broken onBeforeDelete() function.  Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1146         }
1147         
1148         foreach($this->getClassAncestry() as $ancestor) {
1149             if(self::has_own_table($ancestor)) {
1150                 $sql = new SQLQuery();
1151                 $sql->delete = true;
1152                 $sql->from[$ancestor] = "\"$ancestor\"";
1153                 $sql->where[] = "\"ID\" = $this->ID";
1154                 $this->extend('augmentSQL', $sql);
1155                 $sql->execute();
1156             }
1157         }
1158         // Remove this item out of any caches
1159         $this->flushCache();
1160         
1161         $this->onAfterDelete();
1162 
1163         $this->OldID = $this->ID;
1164         $this->ID = 0;
1165 
1166         DataObjectLog::deletedObject($this);
1167     }
1168 
1169     /**
1170      * Delete the record with the given ID.
1171      *
1172      * @param string $className The class name of the record to be deleted
1173      * @param int $id ID of record to be deleted
1174      */
1175     public static function delete_by_id($className, $id) {
1176         $obj = DataObject::get_by_id($className, $id);
1177         if($obj) {
1178             $obj->delete();
1179         } else {
1180             user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1181         }
1182     }
1183 
1184     /**
1185      * A cache used by getClassAncestry()
1186      * @var array
1187      */
1188     protected static $ancestry;
1189 
1190     /**
1191      * Get the class ancestry, including the current class name.
1192      * The ancestry will be returned as an array of class names, where the 0th element
1193      * will be the class that inherits directly from DataObject, and the last element
1194      * will be the current class.
1195      *
1196      * @return array Class ancestry
1197      */
1198     public function getClassAncestry() {
1199         if(!isset(DataObject::$ancestry[$this->class])) {
1200             DataObject::$ancestry[$this->class] = array($this->class);
1201             while(($class = get_parent_class(DataObject::$ancestry[$this->class][0])) != "DataObject") {
1202                 array_unshift(DataObject::$ancestry[$this->class], $class);
1203             }
1204         }
1205         return DataObject::$ancestry[$this->class];
1206     }
1207 
1208     /**
1209      * Return a component object from a one to one relationship, as a DataObject.
1210      * If no component is available, an 'empty component' will be returned.
1211      *
1212      * @param string $componentName Name of the component
1213      *
1214      * @return DataObject The component object. It's exact type will be that of the component.
1215      */
1216     public function getComponent($componentName) {
1217         if(isset($this->components[$componentName])) {
1218             return $this->components[$componentName];
1219         }
1220         
1221         if($class = $this->has_one($componentName)) {
1222             $joinField = $componentName . 'ID';
1223             $joinID    = $this->getField($joinField);
1224             
1225             if($joinID) {
1226                 $component = DataObject::get_by_id($class, $joinID);
1227             }
1228             
1229             if(!isset($component) || !$component) {
1230                 $component = new $class();
1231             }
1232         } elseif($class = $this->belongs_to($componentName)) {
1233             $joinField = $this->getRemoteJoinField($componentName, 'belongs_to');
1234             $joinID    = $this->ID;
1235             
1236             if($joinID) {
1237                 $component = DataObject::get_one($class, "\"$joinField\" = $joinID");
1238             }
1239             
1240             if(!isset($component) || !$component) {
1241                 $component = new $class();
1242                 $component->$joinField = $this->ID;
1243             }
1244         } else {
1245             throw new Exception("DataObject->getComponent(): Could not find component '$componentName'.");
1246         }
1247         
1248         $this->components[$componentName] = $component;
1249         return $component;
1250     }
1251 
1252     /**
1253      * A cache used by component getting classes
1254      * @var array
1255      */
1256     protected $componentCache;
1257 
1258     /**
1259      * Returns a one-to-many component, as a ComponentSet.
1260      *
1261      * @param string $componentName Name of the component
1262      * @param string $filter A filter to be inserted into the WHERE clause
1263      * @param string|array $sort A sort expression to be inserted into the ORDER BY clause. If omitted, the static field $default_sort on the component class will be used.
1264      * @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned.
1265      * @param string|array $limit A limit expression to be inserted into the LIMIT clause
1266      *
1267      * @return ComponentSet The components of the one-to-many relationship.
1268      */
1269     public function getComponents($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
1270         $result = null;
1271 
1272         $sum = md5("{$filter}_{$sort}_{$join}_{$limit}");
1273         if(isset($this->componentCache[$componentName . '_' . $sum]) && false != $this->componentCache[$componentName . '_' . $sum]) {
1274             return $this->componentCache[$componentName . '_' . $sum];
1275         }
1276 
1277         if(!$componentClass = $this->has_many($componentName)) {
1278             user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName' on class '$this->class'", E_USER_ERROR);
1279         }
1280 
1281         $joinField = $this->getRemoteJoinField($componentName, 'has_many');
1282 
1283         if($this->isInDB()) { //Check to see whether we should query the db
1284             $query = $this->getComponentsQuery($componentName, $filter, $sort, $join, $limit);
1285             $result = $this->buildDataObjectSet($query->execute(), 'ComponentSet', $query, $componentClass);
1286             if($result) $result->parseQueryLimit($query);
1287         }
1288 
1289         if(!$result) {
1290             // If this record isn't in the database, then we want to hold onto this specific ComponentSet,
1291             // because it's the only copy of the data that we have.
1292             $result = new ComponentSet();
1293             $this->setComponent($componentName . '_' . $sum, $result);
1294         }
1295 
1296         $result->setComponentInfo("1-to-many", $this, null, null, $componentClass, $joinField);
1297 
1298         return $result;
1299     }
1300 
1301     /**
1302      * Get the query object for a $has_many Component.
1303      *
1304      * Use {@link DataObjectSet->setComponentInfo()} to attach metadata to the
1305      * resultset you're building with this query.
1306      * Use {@link DataObject->buildDataObjectSet()} to build a set out of the {@link SQLQuery}
1307      * object, and pass "ComponentSet" as a $containerClass.
1308      *
1309      * @param string $componentName
1310      * @param string $filter
1311      * @param string|array $sort
1312      * @param string $join
1313      * @param string|array $limit
1314      * @return SQLQuery
1315      */
1316     public function getComponentsQuery($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
1317         if(!$componentClass = $this->has_many($componentName)) {
1318             user_error("DataObject::getComponentsQuery(): Unknown 1-to-many component '$componentName' on class '$this->class'", E_USER_ERROR);
1319         }
1320 
1321         $joinField = $this->getRemoteJoinField($componentName, 'has_many');
1322 
1323         $id = $this->getField("ID");
1324             
1325         // get filter
1326         $combinedFilter = "\"$joinField\" = '$id'";
1327         if($filter) $combinedFilter .= " AND {$filter}";
1328             
1329         return singleton($componentClass)->extendedSQL($combinedFilter, $sort, $limit, $join);
1330     }
1331     
1332     /**
1333      * Tries to find the database key on another object that is used to store a relationship to this class. If no join
1334      * field can be found it defaults to 'ParentID'.
1335      *
1336      * @param string $component
1337      * @param string $type the join type - either 'has_many' or 'belongs_to'
1338      * @return string
1339      */
1340     public function getRemoteJoinField($component, $type = 'has_many') {
1341         $remoteClass = $this->$type($component, false);
1342         
1343         if(!$remoteClass) {
1344             throw new Exception("Unknown $type component '$component' on class '$this->class'");
1345         }
1346         
1347         if($fieldPos = strpos($remoteClass, '.')) {
1348             return substr($remoteClass, $fieldPos + 1) . 'ID';
1349         }
1350         
1351         $remoteRelations = array_flip(Object::combined_static($remoteClass, 'has_one', 'DataObject'));
1352         
1353         // look for remote has_one joins on this class or any parent classes
1354         foreach(array_reverse(ClassInfo::ancestry($this)) as $class) {
1355             if(array_key_exists($class, $remoteRelations)) return $remoteRelations[$class] . 'ID';
1356         }
1357         
1358         return 'ParentID';
1359     }
1360     
1361     /**
1362      * Sets the component of a relationship.
1363      *
1364      * @param string $componentName Name of the component
1365      * @param DataObject|ComponentSet $componentValue Value of the component
1366      */
1367     public function setComponent($componentName, $componentValue) {
1368         $this->componentCache[$componentName] = $componentValue;
1369     }
1370 
1371     /**
1372      * Returns a many-to-many component, as a ComponentSet.
1373      * @param string $componentName Name of the many-many component
1374      * @return ComponentSet The set of components
1375      *
1376      * @todo Implement query-params
1377      */
1378     public function getManyManyComponents($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
1379         $sum = md5("{$filter}_{$sort}_{$join}_{$limit}");
1380         if(isset($this->componentCache[$componentName . '_' . $sum]) && false != $this->componentCache[$componentName . '_' . $sum]) {
1381             return $this->componentCache[$componentName . '_' . $sum];
1382         }
1383 
1384         list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName);
1385 
1386         // Join expression is done on SiteTree.ID even if we link to Page; it helps work around
1387         // database inconsistencies
1388         $componentBaseClass = ClassInfo::baseDataClass($componentClass);
1389 
1390         if($this->ID && is_numeric($this->ID)) {
1391                 
1392             if($componentClass) {
1393                 $query = $this->getManyManyComponentsQuery($componentName, $filter, $sort, $join, $limit);
1394                 $records = $query->execute();
1395                 $result = $this->buildDataObjectSet($records, "ComponentSet", $query, $componentBaseClass);
1396                 if($result) $result->parseQueryLimit($query); // for pagination support
1397                 if(!$result) {
1398                     $result = new ComponentSet();
1399                 }
1400             }
1401         } else {
1402             $result = new ComponentSet();
1403         }
1404         $result->setComponentInfo("many-to-many", $this, $parentClass, $table, $componentClass);
1405 
1406         // If this record isn't in the database, then we want to hold onto this specific ComponentSet,
1407         // because it's the only copy of the data that we have.
1408         if(!$this->isInDB()) {
1409             $this->setComponent($componentName . '_' . $sum, $result);
1410         }
1411 
1412         return $result;
1413     }
1414 
1415     /**
1416      * Get the query object for a $many_many Component.
1417      * Use {@link DataObjectSet->setComponentInfo()} to attach metadata to the
1418      * resultset you're building with this query.
1419      * Use {@link DataObject->buildDataObjectSet()} to build a set out of the {@link SQLQuery}
1420      * object, and pass "ComponentSet" as a $containerClass.
1421      *
1422      * @param string $componentName
1423      * @param string $filter
1424      * @param string|array $sort
1425      * @param string $join
1426      * @param string|array $limit
1427      * @return SQLQuery
1428      */
1429     public function getManyManyComponentsQuery($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
1430         list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName);
1431 
1432         $componentObj = singleton($componentClass);
1433 
1434         // Join expression is done on SiteTree.ID even if we link to Page; it helps work around
1435         // database inconsistencies
1436         $componentBaseClass = ClassInfo::baseDataClass($componentClass);
1437 
1438 
1439         $query = $componentObj->extendedSQL(
1440             "\"$table\".\"$parentField\" = $this->ID", // filter 
1441             $sort,
1442             $limit,
1443             "INNER JOIN \"$table\" ON \"$table\".\"$componentField\" = \"$componentBaseClass\".\"ID\"" // join
1444         );
1445         
1446         foreach((array)$this->many_many_extraFields($componentName) as $extraField => $extraFieldType) {
1447             $query->select[] = "\"$table\".\"$extraField\"";
1448             $query->groupby[] = "\"$table\".\"$extraField\"";
1449         }
1450 
1451         if($filter) $query->where[] = $filter;
1452         if($join) $query->from[] = $join;
1453 
1454         return $query;
1455     }
1456 
1457     /**
1458      * Pull out a join clause for a many-many relationship.
1459      *
1460      * @param string $componentName The many_many or belongs_many_many relation to join to.
1461      * @param string $baseTable The classtable that will already be included in the SQL query to which this join will be added.
1462      * @return string SQL join clause
1463      */
1464     function getManyManyJoin($componentName, $baseTable) {
1465         if(!$componentClass = $this->many_many($componentName)) {
1466             user_error("DataObject::getComponents(): Unknown many-to-many component '$componentName' on class '$this->class'", E_USER_ERROR);
1467         }
1468         $classes = array_reverse(ClassInfo::ancestry($this->class));
1469 
1470         list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName);
1471 
1472         $baseComponentClass = ClassInfo::baseDataClass($componentClass);
1473         if($baseTable == $parentClass) {
1474             return "LEFT JOIN \"$table\" ON (\"$table\".\"$parentField\" = \"$parentClass\".\"ID\" AND \"$table\".\"$componentField\" = '{$this->ID}')";
1475         } else {
1476             return "LEFT JOIN \"$table\" ON (\"$table\".\"$componentField\" = \"$baseComponentClass\".\"ID\" AND \"$table\".\"$parentField\" = '{$this->ID}')";
1477         }
1478     }
1479 
1480     function getManyManyFilter($componentName, $baseTable) {
1481         list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName);
1482 
1483         return "\"$table\".\"$parentField\" = '{$this->ID}'";
1484     }
1485 
1486     /**
1487      * Return an aggregate object. An aggregate object returns the result of running some SQL aggregate function on a field of 
1488      * this dataobject type.
1489      * 
1490      * It can be called with no arguments, in which case it returns an object that calculates aggregates on this object's type,
1491      * or with an argument (possibly statically), in which case it returns an object for that type
1492      */
1493     function Aggregate($type = null, $filter = '') {
1494         return new Aggregate($type ? $type : $this->class, $filter);
1495     }
1496     
1497     /**
1498      * Return an relationship aggregate object. A relationship aggregate does the same thing as an aggregate object, but operates
1499      * on a has_many rather than directly on the type specified
1500      */
1501     function RelationshipAggregate($object = null, $relationship = '', $filter = '') {
1502         if (is_string($object)) { $filter = $relationship; $relationship = $object; $object = $this; }
1503         return new Aggregate_Relationship($object ? $object : $this->owner, $relationship, $filter);
1504     }
1505     
1506     /**
1507      * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and their classes.
1508      *
1509      * @param string $component Name of component
1510      *
1511      * @return string|array The class of the one-to-one component, or an array of all one-to-one components and their classes.
1512      */
1513     public function has_one($component = null) {
1514         $classes = ClassInfo::ancestry($this);
1515 
1516         foreach($classes as $class) {
1517             // Wait until after we reach DataObject
1518             if(in_array($class, array('Object', 'ViewableData', 'DataObject'))) continue;
1519 
1520             if($component) {
1521                 $hasOne = Object::uninherited_static($class, 'has_one');
1522                 
1523                 if(isset($hasOne[$component])) {
1524                     return $hasOne[$component];
1525                 }
1526             } else {
1527                 $newItems = (array) Object::uninherited_static($class, 'has_one');
1528                 // Validate the data
1529                 foreach($newItems as $k => $v) {
1530                     if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$has_one has a bad entry: " 
1531                         . var_export($k,true). " => " . var_export($v,true) . ".  Each map key should be a relationship name, and the map value should be the data class to join to.", E_USER_ERROR);
1532                 }
1533                 $items = isset($items) ? array_merge($newItems, (array)$items) : $newItems;
1534             }
1535         }
1536         return isset($items) ? $items : null;
1537     }
1538     
1539     /**
1540      * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1541      * their class name will be returned.
1542      *
1543      * @param string $component
1544      * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1545      *        the field data stripped off. It defaults to TRUE.
1546      * @return string|array
1547      */
1548     public function belongs_to($component = null, $classOnly = true) {
1549         $belongsTo = Object::combined_static($this->class, 'belongs_to', 'DataObject');
1550         
1551         if($component) {
1552             if($belongsTo && array_key_exists($component, $belongsTo)) {
1553                 $belongsTo = $belongsTo[$component];
1554             } else {
1555                 return false;
1556             }
1557         }
1558         
1559         if($belongsTo && $classOnly) {
1560             return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1561         } else {
1562             return $belongsTo ? $belongsTo : array();
1563         }
1564     }
1565     
1566     /**
1567      * Return all of the database fields defined in self::$db and all the parent classes.
1568      * Doesn't include any fields specified by self::$has_one.  Use $this->has_one() to get these fields
1569      *
1570      * @param string $fieldName Limit the output to a specific field name
1571      * @return array The database fields
1572      */
1573     public function db($fieldName = null) {
1574         $classes = ClassInfo::ancestry($this);
1575         $good = false;
1576         $items = array();
1577         
1578         foreach($classes as $class) {
1579             // Wait until after we reach DataObject
1580             if(!$good) {
1581                 if($class == 'DataObject') {
1582                     $good = true;
1583                 }
1584                 continue;
1585             }
1586 
1587             if($fieldName) {
1588                 $db = Object::uninherited_static($class, 'db');
1589                 
1590                 if(isset($db[$fieldName])) {
1591                     return $db[$fieldName];
1592                 }
1593             } else {
1594                 $newItems = (array) Object::uninherited_static($class, 'db');
1595                 // Validate the data
1596                 foreach($newItems as $k => $v) {
1597                     if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$db has a bad entry: " 
1598                         . var_export($k,true). " => " . var_export($v,true) . ".  Each map key should be a property name, and the map value should be the property type.", E_USER_ERROR);
1599                 }
1600                 $items = isset($items) ? array_merge((array)$items, $newItems) : $newItems;
1601             }
1602         }
1603 
1604         return $items;
1605     }
1606 
1607     /**
1608      * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
1609      * relationships and their classes will be returned.
1610      *
1611      * @param string $component Name of component
1612      * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1613      *        the field data stripped off. It defaults to TRUE.
1614      * @return string|array
1615      */
1616     public function has_many($component = null, $classOnly = true) {
1617         $hasMany = Object::combined_static($this->class, 'has_many', 'DataObject');
1618         
1619         if($component) {
1620             if($hasMany && array_key_exists($component, $hasMany)) {
1621                 $hasMany = $hasMany[$component];
1622             } else {
1623                 return false;
1624             }
1625         }
1626         
1627         if($hasMany && $classOnly) {
1628             return preg_replace('/(.+)?\..+/', '$1', $hasMany);
1629         } else {
1630             return $hasMany ? $hasMany : array();
1631         }
1632     }
1633 
1634     /**
1635      * Return the many-to-many extra fields specification.
1636      * 
1637      * If you don't specify a component name, it returns all
1638      * extra fields for all components available.
1639      * 
1640      * @param string $component Name of component
1641      * @return array
1642      */
1643     public function many_many_extraFields($component = null) {
1644         $classes = ClassInfo::ancestry($this);
1645 
1646         foreach($classes as $class) {
1647             if(in_array($class, array('ViewableData', 'Object', 'DataObject'))) continue;
1648             
1649             // Find extra fields for one component
1650             if($component) {
1651                 $SNG_class = singleton($class);
1652                 $extraFields = $SNG_class->stat('many_many_extraFields');
1653 
1654                 // Extra fields are immediately available on this class
1655                 if(isset($extraFields[$component])) {
1656                     return $extraFields[$component];
1657                 }
1658                 
1659                 $manyMany = $SNG_class->stat('many_many');
1660                 $candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
1661                 if($candidate) {
1662                     $SNG_candidate = singleton($candidate);
1663                     $candidateManyMany = $SNG_candidate->stat('belongs_many_many');
1664                     
1665                     // Find the relation given the class
1666                     $relationName = null;
1667                     if($candidateManyMany) foreach($candidateManyMany as $relation => $relatedClass) {
1668                         if($relatedClass == $class) {
1669                             $relationName = $relation;
1670                             break;
1671                         }
1672                     }
1673                     
1674                     if($relationName) {
1675                         $extraFields = $SNG_candidate->stat('many_many_extraFields');
1676                         if(isset($extraFields[$relationName])) {
1677                             return $extraFields[$relationName];
1678                         }
1679                     }
1680                 }
1681                                 
1682                 $manyMany = $SNG_class->stat('belongs_many_many');
1683                 $candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
1684                 if($candidate) {
1685                     $SNG_candidate = singleton($candidate);
1686                     $candidateManyMany = $SNG_candidate->stat('many_many');
1687                     
1688                     // Find the relation given the class
1689                     if($candidateManyMany) foreach($candidateManyMany as $relation => $relatedClass) {
1690                         if($relatedClass == $class) {
1691                             $relationName = $relation;
1692                         }
1693                     }
1694                     
1695                     $extraFields = $SNG_candidate->stat('many_many_extraFields');
1696                     if(isset($extraFields[$relationName])) {
1697                         return $extraFields[$relationName];
1698                     }
1699                 }
1700                 
1701             } else {
1702                 
1703                 // Find all the extra fields for all components
1704                 $newItems = eval("return (array){$class}::\$many_many_extraFields;");
1705                 
1706                 foreach($newItems as $k => $v) {
1707                     if(!is_array($v)) {
1708                         user_error(
1709                             "$class::\$many_many_extraFields has a bad entry: "
1710                             . var_export($k, true) . " => " . var_export($v, true)
1711                             . ". Each many_many_extraFields entry should map to a field specification array.",
1712                             E_USER_ERROR
1713                         );
1714                     }
1715                 }
1716                     
1717                 return isset($items) ? array_merge($newItems, $items) : $newItems;
1718             }
1719         }
1720     }
1721     
1722     /**
1723      * Return information about a many-to-many component.
1724      * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
1725      * components are returned.
1726      *
1727      * @param string $component Name of component
1728      *
1729      * @return array  An array of (parentclass, childclass), or an array of all many-many components
1730      */
1731     public function many_many($component = null) {
1732         $classes = ClassInfo::ancestry($this);
1733 
1734         foreach($classes as $class) {
1735             // Wait until after we reach DataObject
1736             if(in_array($class, array('ViewableData', 'Object', 'DataObject'))) continue;
1737 
1738             if($component) {
1739                 $manyMany = Object::uninherited_static($class, 'many_many');
1740                 // Try many_many
1741                 $candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
1742                 if($candidate) {
1743                     $parentField = $class . "ID";
1744                     $childField = ($class == $candidate) ? "ChildID" : $candidate . "ID";
1745                     return array($class, $candidate, $parentField, $childField, "{$class}_$component");
1746                 }
1747 
1748                 // Try belongs_many_many
1749                 $belongsManyMany = Object::uninherited_static($class, 'belongs_many_many');
1750                 $candidate = (isset($belongsManyMany[$component])) ? $belongsManyMany[$component] : null;
1751                 if($candidate) {
1752                     $childField = $candidate . "ID";
1753 
1754                     // We need to find the inverse component name
1755                     $otherManyMany = Object::uninherited_static($candidate, 'many_many');
1756                     if(!$otherManyMany) {
1757                         user_error("Inverse component of $candidate not found ({$this->class})", E_USER_ERROR);
1758                     }
1759 
1760                     foreach($otherManyMany as $inverseComponentName => $candidateClass) {
1761                         if($candidateClass == $class || is_subclass_of($class, $candidateClass)) {
1762                             $parentField = ($class == $candidate) ? "ChildID" : $candidateClass . "ID";
1763                             // HACK HACK HACK!
1764                             if($component == 'NestedProducts') {
1765                                 $parentField = $candidateClass . "ID";
1766                             }
1767 
1768                             return array($class, $candidate, $parentField, $childField, "{$candidate}_$inverseComponentName");
1769                         }
1770                     }
1771                     user_error("Orphaned \$belongs_many_many value for $this->class.$component", E_USER_ERROR);
1772                 }
1773             } else {
1774                 $newItems = (array) Object::uninherited_static($class, 'many_many');
1775                 // Validate the data
1776                 foreach($newItems as $k => $v) {
1777                     if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$many_many has a bad entry: " 
1778                         . var_export($k,true). " => " . var_export($v,true) . ".  Each map key should be a relationship name, and the map value should be the data class to join to.", E_USER_ERROR);
1779                 }
1780                 $items = isset($items) ? array_merge($newItems, $items) : $newItems;
1781                 
1782                 $newItems = (array) Object::uninherited_static($class, 'belongs_many_many');
1783                 // Validate the data
1784                 foreach($newItems as $k => $v) {
1785                     if(!is_string($k) || is_numeric($k) || !is_string($v)) user_error("$class::\$belongs_many_many has a bad entry: " 
1786                         . var_export($k,true). " => " . var_export($v,true) . ".  Each map key should be a relationship name, and the map value should be the data class to join to.", E_USER_ERROR);
1787                 }
1788 
1789                 $items = isset($items) ? array_merge($newItems, $items) : $newItems;
1790             }
1791         }
1792         
1793         return isset($items) ? $items : null;
1794     }
1795     
1796     /**
1797      * This returns an array (if it exists) describing the database extensions that are required, or false if none
1798      * 
1799      * This is experimental, and is currently only a Postgres-specific enhancement.
1800      * 
1801      * @return array or false
1802      */
1803     function database_extensions($class){
1804         
1805         $extensions = Object::uninherited_static($class, 'database_extensions');
1806         
1807         if($extensions)
1808             return $extensions;
1809         else
1810             return false;
1811     }
1812 
1813     /**
1814      * Generates a SearchContext to be used for building and processing
1815      * a generic search form for properties on this object.
1816      *
1817      * @return SearchContext
1818      */
1819     public function getDefaultSearchContext() {
1820         return new SearchContext(
1821             $this->class, 
1822             $this->scaffoldSearchFields(), 
1823             $this->defaultSearchFilters()
1824         );
1825     }
1826     
1827     /**
1828      * Determine which properties on the DataObject are
1829      * searchable, and map them to their default {@link FormField}
1830      * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
1831      *
1832      * Some additional logic is included for switching field labels, based on
1833      * how generic or specific the field type is.
1834      *
1835      * Used by {@link SearchContext}.
1836      * 
1837      * @param array $_params
1838      *  'fieldClasses': Associative array of field names as keys and FormField classes as values
1839      *  'restrictFields': Numeric array of a field name whitelist
1840      * @return FieldSet
1841      */
1842     public function scaffoldSearchFields($_params = null) {
1843         $params = array_merge(
1844             array(
1845                 'fieldClasses' => false,
1846                 'restrictFields' => false
1847             ),
1848             (array)$_params
1849         );
1850         $fields = new FieldSet();
1851         foreach($this->searchableFields() as $fieldName => $spec) {
1852             if($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) continue;
1853             
1854             // If a custom fieldclass is provided as a string, use it
1855             if($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
1856                 $fieldClass = $params['fieldClasses'][$fieldName];
1857                 $field = new $fieldClass($fieldName);
1858             // If we explicitly set a field, then construct that
1859             } else if(isset($spec['field'])) {
1860                 // If it's a string, use it as a class name and construct
1861                 if(is_string($spec['field'])) {
1862                     $fieldClass = $spec['field'];
1863                     $field = new $fieldClass($fieldName);
1864                     
1865                 // If it's a FormField object, then just use that object directly.
1866                 } else if($spec['field'] instanceof FormField) {
1867                     $field = $spec['field'];
1868                     
1869                 // Otherwise we have a bug
1870                 } else {
1871                     user_error("Bad value for searchable_fields, 'field' value: " . var_export($spec['field'], true), E_USER_WARNING);
1872                 }
1873                 
1874             // Otherwise, use the database field's scaffolder
1875             } else {
1876                 $field = $this->relObject($fieldName)->scaffoldSearchField();
1877             }
1878 
1879             if (strstr($fieldName, '.')) {
1880                 $field->setName(str_replace('.', '__', $fieldName));
1881             }
1882             $field->setTitle($spec['title']);
1883 
1884             $fields->push($field);
1885         }
1886         return $fields;
1887     }
1888 
1889     /**
1890      * Scaffold a simple edit form for all properties on this dataobject,
1891      * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
1892      * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
1893      *
1894      * @uses FormScaffolder
1895      * 
1896      * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
1897      * @return FieldSet
1898      */
1899     public function scaffoldFormFields($_params = null) {
1900         $params = array_merge(
1901             array(
1902                 'tabbed' => false,
1903                 'includeRelations' => false,
1904                 'restrictFields' => false,
1905                 'fieldClasses' => false,
1906                 'ajaxSafe' => false
1907             ),
1908             (array)$_params
1909         );
1910         
1911         $fs = new FormScaffolder($this);
1912         $fs->tabbed = $params['tabbed'];
1913         $fs->includeRelations = $params['includeRelations'];
1914         $fs->restrictFields = $params['restrictFields'];
1915         $fs->fieldClasses = $params['fieldClasses'];
1916         $fs->ajaxSafe = $params['ajaxSafe'];
1917         
1918         return $fs->getFieldSet();
1919     }
1920     
1921     /**
1922      * Centerpiece of every data administration interface in Silverstripe,
1923      * which returns a {@link FieldSet} suitable for a {@link Form} object.
1924      * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
1925      * generate this set. To customize, overload this method in a subclass
1926      * or decorate onto it by using {@link DataObjectDecorator->updateCMSFields()}.
1927      *
1928      * <code>
1929      * klass MyCustomClass extends DataObject {
1930      *  static $db = array('CustomProperty'=>'Boolean');
1931      *
1932      *  public function getCMSFields() {
1933      *      $fields = parent::getCMSFields();
1934      *      $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
1935      *      return $fields;
1936      *  }
1937      * }
1938      * </code>
1939      *
1940      * @see Good example of complex FormField building: SiteTree::getCMSFields()
1941      *
1942      * @param array $params See {@link scaffoldFormFields()}
1943      * @return FieldSet Returns a TabSet for usage within the CMS - don't use for frontend forms.
1944      */
1945     public function getCMSFields($params = null) {
1946         $tabbedFields = $this->scaffoldFormFields(array_merge(
1947             array(
1948                 'includeRelations' => true,
1949                 'tabbed' => true,
1950                 'ajaxSafe' => true
1951             ),
1952             (array)$params
1953         ));
1954         
1955         if(self::$runCMSFieldsExtensions) {
1956             $this->extend('updateCMSFields', $tabbedFields);
1957         }
1958         
1959         return $tabbedFields;
1960     }
1961     
1962     /**
1963      * need to be overload by solid dataobject, so that the customised actions of that dataobject,
1964      * including that dataobject's decorator customised actions could be added to the EditForm.
1965      * 
1966      * @return an Empty FieldSet(); need to be overload by solid subclass
1967      */
1968     public function getCMSActions() {
1969         $actions = new FieldSet();
1970         $this->extend('updateCMSActions', $actions);
1971         return $actions;
1972     }
1973     
1974 
1975     /**
1976      * Used for simple frontend forms without relation editing
1977      * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
1978      * by default. To customize, either overload this method in your
1979      * subclass, or decorate it by {@link DataObjectDecorator->updateFrontEndFields()}.
1980      * 
1981      * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
1982      *
1983      * @param array $params See {@link scaffoldFormFields()}
1984      * @return FieldSet Always returns a simple field collection without TabSet.
1985      */
1986     public function getFrontEndFields($params = null) {
1987         $untabbedFields = $this->scaffoldFormFields($params);
1988         $this->extend('updateFrontEndFields', $untabbedFields);
1989     
1990         return $untabbedFields;
1991     }
1992 
1993     /**
1994      * Gets the value of a field.
1995      * Called by {@link __get()} and any getFieldName() methods you might create.
1996      *
1997      * @param string $field The name of the field
1998      *
1999      * @return mixed The field value
2000      */
2001     public function getField($field) {
2002         // If we already have an object in $this->record, then we should just return that
2003         if(isset($this->record[$field]) && is_object($this->record[$field]))  return $this->record[$field];
2004         
2005         // Otherwise, we need to determine if this is a complex field
2006         if(self::is_composite_field($this->class, $field)) {
2007             $helper = $this->castingHelper($field);
2008             $fieldObj = Object::create_from_string($helper, $field);
2009             
2010             // write value only if either the field value exists,
2011             // or a valid record has been loaded from the database
2012             $value = (isset($this->record[$field])) ? $this->record[$field] : null;
2013             if($value || $this->exists()) $fieldObj->setValue($value, $this->record, false);
2014             
2015             $this->record[$field] = $fieldObj;
2016 
2017             return $this->record[$field];
2018         }
2019 
2020         return isset($this->record[$field]) ? $this->record[$field] : null;
2021     }
2022 
2023     /**
2024      * Return a map of all the fields for this record.
2025      *
2026      * @return array A map of field names to field values.
2027      */
2028     public function getAllFields() {
2029         return $this->record;
2030     }
2031 
2032     /**
2033      * Return the fields that have changed.
2034      * 
2035      * The change level affects what the functions defines as "changed":
2036      * - Level 1 will return strict changes, even !== ones.
2037      * - Level 2 is more lenient, it will only return real data changes, for example a change from 0 to null
2038      * would not be included.
2039      *
2040      * Example return:
2041      * <code>
2042      * array(
2043      *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => 2)
2044      * )
2045      * </code>
2046      *
2047      * @param boolean $databaseFieldsOnly Get only database fields that have changed
2048      * @param int $changeLevel The strictness of what is defined as change
2049      * @return array
2050      */
2051     public function getChangedFields($databaseFieldsOnly = false, $changeLevel = 1) {
2052         $changedFields = array();
2053         
2054         // Update the changed array with references to changed obj-fields
2055         foreach($this->record as $k => $v) {
2056             if(is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2057                 $this->changed[$k] = 1;
2058             }
2059         }
2060         
2061         if($databaseFieldsOnly) {
2062             $databaseFields = $this->inheritedDatabaseFields();
2063             $databaseFields['ID'] = true;
2064             $databaseFields['LastEdited'] = true;
2065             $databaseFields['Created'] = true;
2066             $databaseFields['ClassName'] = true;
2067             $fields = array_intersect_key((array)$this->changed, $databaseFields);
2068         } else {
2069             $fields = $this->changed;
2070         }
2071 
2072         // Filter the list to those of a certain change level
2073         if($changeLevel > 1) {
2074             if($fields) foreach($fields as $name => $level) {
2075                 if($level < $changeLevel) {
2076                     unset($fields[$name]);
2077                 }
2078             }
2079         }
2080         
2081         if($fields) foreach($fields as $name => $level) {
2082             $changedFields[$name] = array(
2083                 'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2084                 'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2085                 'level' => $level
2086             );
2087         }
2088 
2089         return $changedFields;
2090     }
2091     
2092     /**
2093      * Uses {@link getChangedFields()} to determine if fields have been changed
2094      * since loading them from the database.
2095      * 
2096      * @param string $fieldName Name of the database field to check, will check for any if not given
2097      * @param int $changeLevel See {@link getChangedFields()}
2098      * @return boolean
2099      */
2100     function isChanged($fieldName = null, $changeLevel = 1) {
2101         $changed = $this->getChangedFields(false, $changeLevel);
2102         if(!isset($fieldName)) {
2103             return !empty($changed);
2104         } 
2105         else {
2106             return array_key_exists($fieldName, $changed);
2107         }
2108     }
2109 
2110     /**
2111      * Set the value of the field
2112      * Called by {@link __set()} and any setFieldName() methods you might create.
2113      *
2114      * @param string $fieldName Name of the field
2115      * @param mixed $val New field value
2116      */
2117     function setField($fieldName, $val) {
2118         // Situation 1: Passing an DBField
2119         if($val instanceof DBField) {
2120             $val->Name = $fieldName;
2121             $this->record[$fieldName] = $val;
2122         // Situation 2: Passing a literal or non-DBField object
2123         } else {
2124             // If this is a proper database field, we shouldn't be getting non-DBField objects
2125             if(is_object($val) && $this->db($fieldName)) {
2126                 user_error('DataObject::setField: passed an object that is not a DBField', E_USER_WARNING);
2127             }
2128         
2129             $defaults = $this->stat('defaults');
2130             // if a field is not existing or has strictly changed
2131             if(!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2132                 // TODO Add check for php-level defaults which are not set in the db
2133                 // TODO Add check for hidden input-fields (readonly) which are not set in the db
2134                 // At the very least, the type has changed
2135                 $this->changed[$fieldName] = 1;
2136                 
2137                 if((!isset($this->record[$fieldName]) && $val) || (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)) {
2138                     // Value has changed as well, not just the type
2139                     $this->changed[$fieldName] = 2;
2140                 }
2141 
2142                 // value is always saved back when strict check succeeds
2143                 $this->record[$fieldName] = $val;
2144             }
2145         }
2146     }
2147 
2148     /**
2149      * Set the value of the field, using a casting object.
2150      * This is useful when you aren't sure that a date is in SQL format, for example.
2151      * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2152      * can be saved into the Image table.
2153      *
2154      * @param string $fieldName Name of the field
2155      * @param mixed $value New field value
2156      */
2157     public function setCastedField($fieldName, $val) {
2158         if(!$fieldName) {
2159             user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2160         }
2161         $castingHelper = $this->castingHelper($fieldName);
2162         if($castingHelper) {
2163             $fieldObj = Object::create_from_string($castingHelper, $fieldName);
2164             $fieldObj->setValue($val);
2165             $fieldObj->saveInto($this);
2166         } else {
2167             $this->$fieldName = $val;
2168         }
2169     }
2170 
2171     /**
2172      * Returns true if the given field exists
2173      * in a database column on any of the objects tables,
2174      * or as a dynamic getter with get<fieldName>().
2175      *
2176      * @param string $field Name of the field
2177      * @return boolean True if the given field exists
2178      */
2179     public function hasField($field) {
2180         return (
2181             array_key_exists($field, $this->record) 
2182             || $this->db($field)
2183             || $this->hasMethod("get{$field}")
2184         );
2185     }
2186 
2187     /**
2188      * Returns true if the given field exists as a database column
2189      *
2190      * @param string $field Name of the field
2191      *
2192      * @return boolean
2193      */
2194     public function hasDatabaseField($field) {
2195         // Add base fields which are not defined in static $db
2196         static $fixedFields = array(
2197             'ID' => 'Int',
2198             'ClassName' => 'Enum',
2199             'LastEdited' => 'SS_Datetime',
2200             'Created' => 'SS_Datetime',
2201         );
2202         
2203         if(isset($fixedFields[$field])) return true;
2204 
2205         return array_key_exists($field, $this->inheritedDatabaseFields());
2206     }
2207     
2208     /**
2209      * Returns the field type of the given field, if it belongs to this class, and not a parent.
2210      * Note that the field type will not include constructor arguments in round brackets, only the classname.
2211      *
2212      * @param string $field Name of the field
2213      * @return string The field type of the given field
2214      */
2215     public function hasOwnTableDatabaseField($field) {
2216         // Add base fields which are not defined in static $db
2217         if($field == "ID") return "Int";
2218         if($field == "ClassName" && get_parent_class($this) == "DataObject") return "Enum";
2219         if($field == "LastEdited" && get_parent_class($this) == "DataObject") return "SS_Datetime";
2220         if($field == "Created" && get_parent_class($this) == "DataObject") return "SS_Datetime";
2221 
2222         // Add fields from Versioned decorator
2223         if($field == "Version") return $this->hasExtension('Versioned') ? "Int" : false;
2224         
2225         // get cached fieldmap
2226         $fieldMap = isset(self::$cache_has_own_table_field[$this->class]) ? self::$cache_has_own_table_field[$this->class] : null;
2227         
2228         // if no fieldmap is cached, get all fields
2229         if(!$fieldMap) {
2230             $fieldMap = Object::uninherited_static($this->class, 'db');
2231             
2232             // all $db fields on this specific class (no parents)
2233             foreach(self::composite_fields($this->class, false) as $fieldname => $fieldtype) {
2234                 $combined_db = singleton($fieldtype)->compositeDatabaseFields();
2235                 foreach($combined_db as $name => $type){
2236                     $fieldMap[$fieldname.$name] = $type;
2237                 }
2238             }
2239             
2240             // all has_one relations on this specific class,
2241             // add foreign key
2242             $hasOne = Object::uninherited_static($this->class, 'has_one');
2243             if($hasOne) foreach($hasOne as $fieldName => $fieldSchema) {
2244                 $fieldMap[$fieldName . 'ID'] = "ForeignKey";
2245             }
2246 
2247             // set cached fieldmap
2248             self::$cache_has_own_table_field[$this->class] = $fieldMap;
2249         }
2250 
2251         // Remove string-based "constructor-arguments" from the DBField definition
2252         if(isset($fieldMap[$field])) {
2253             if(is_string($fieldMap[$field])) return strtok($fieldMap[$field],'(');
2254             else return $fieldMap[$field]['type'];
2255         }
2256     }
2257     
2258     /**
2259      * Returns true if given class has its own table. Uses the rules for whether the table should exist rather than
2260      * actually looking in the database.
2261      *
2262      * @param string $dataClass
2263      * @return bool
2264      */
2265     public static function has_own_table($dataClass) {
2266         
2267         // The condition below has the same effect as !is_subclass_of($dataClass,'DataObject'),
2268         // which causes PHP < 5.3 to segfault in rare circumstances, see PHP bug #46753
2269         if($dataClass == 'DataObject' || !in_array('DataObject', ClassInfo::ancestry($dataClass))) return false;
2270         
2271         if(!isset(self::$cache_has_own_table[$dataClass])) {
2272             if(get_parent_class($dataClass) == 'DataObject') {
2273                 self::$cache_has_own_table[$dataClass] = true;
2274             } else {
2275                 self::$cache_has_own_table[$dataClass] = Object::uninherited_static($dataClass, 'db') || Object::uninherited_static($dataClass, 'has_one');
2276             }
2277         }
2278         return self::$cache_has_own_table[$dataClass];
2279     }
2280     
2281     /**
2282      * Returns true if the member is allowed to do the given action.
2283      *
2284      * @param string $perm The permission to be checked, such as 'View'.
2285      * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2286      * in user.
2287      *
2288      * @return boolean True if the the member is allowed to do the given action
2289      */
2290     function can($perm, $member = null) {
2291         if(!isset($member)) {
2292             $member = Member::currentUser();
2293         }
2294         if(Permission::checkMember($member, "ADMIN")) return true;
2295 
2296         if($this->many_many('Can' . $perm)) {
2297             if($this->ParentID && $this->SecurityType == 'Inherit') {
2298                 if(!($p = $this->Parent)) {
2299                     return false;
2300                 }
2301                 return $this->Parent->can($perm, $member);
2302 
2303             } else {
2304                 $permissionCache = $this->uninherited('permissionCache');
2305                 $memberID = $member ? $member->ID : 'none';
2306 
2307                 if(!isset($permissionCache[$memberID][$perm])) {
2308                     if($member->ID) {
2309                         $groups = $member->Groups();
2310                     }
2311 
2312                     $groupList = implode(', ', $groups->column("ID"));
2313 
2314                     $query = new SQLQuery(
2315                         "\"Page_Can$perm\".PageID",
2316                     array("\"Page_Can$perm\""),
2317                         "GroupID IN ($groupList)");
2318 
2319                     $permissionCache[$memberID][$perm] = $query->execute()->column();
2320 
2321                     if($perm == "View") {
2322                         $query = new SQLQuery("\"SiteTree\".\"ID\"", array(
2323                             "\"SiteTree\"",
2324                             "LEFT JOIN \"Page_CanView\" ON \"Page_CanView\".\"PageID\" = \"SiteTree\".\"ID\""
2325                             ), "\"Page_CanView\".\"PageID\" IS NULL");
2326 
2327                             $unsecuredPages = $query->execute()->column();
2328                             if($permissionCache[$memberID][$perm]) {
2329                                 $permissionCache[$memberID][$perm] = array_merge($permissionCache[$memberID][$perm], $unsecuredPages);
2330                             } else {
2331                                 $permissionCache[$memberID][$perm] = $unsecuredPages;
2332                             }
2333                     }
2334 
2335                     $this->set_uninherited('permissionCache', $permissionCache);
2336                 }
2337 
2338 
2339                 if($permissionCache[$memberID][$perm]) {
2340                     return in_array($this->ID, $permissionCache[$memberID][$perm]);
2341                 }
2342             }
2343         } else {
2344             return parent::can($perm, $member);
2345         }
2346     }
2347 
2348     /**
2349      * @param Member $member
2350      * @return boolean
2351      */
2352     public function canView($member = null) {
2353         return Permission::check('ADMIN', 'any', $member);
2354     }
2355 
2356     /**
2357      * @param Member $member
2358      * @return boolean
2359      */
2360     public function canEdit($member = null) {
2361         return Permission::check('ADMIN', 'any', $member);
2362     }
2363 
2364     /**
2365      * @param Member $member
2366      * @return boolean
2367      */
2368     public function canDelete($member = null) {
2369         return Permission::check('ADMIN', 'any', $member);
2370     }
2371 
2372     /**
2373      * @todo Should canCreate be a static method?
2374      *
2375      * @param Member $member
2376      * @return boolean
2377      */
2378     public function canCreate($member = null) {
2379         return Permission::check('ADMIN', 'any', $member);
2380     }
2381 
2382     /**
2383      * Debugging used by Debug::show()
2384      *
2385      * @return string HTML data representing this object
2386      */
2387     public function debug() {
2388         $val = "<h3>Database record: $this->class</h3>\n<ul>\n";
2389         if($this->record) foreach($this->record as $fieldName => $fieldVal) {
2390             $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2391         }
2392         $val .= "</ul>\n";
2393         return $val;
2394     }
2395 
2396     /**
2397      * Return the DBField object that represents the given field.
2398      * This works similarly to obj() with 2 key differences:
2399      *   - it still returns an object even when the field has no value.
2400      *   - it only matches fields and not methods
2401      *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2402      *
2403      * @param string $fieldName Name of the field
2404      * @return DBField The field as a DBField object
2405      */
2406     public function dbObject($fieldName) {
2407         // If we have a CompositeDBField object in $this->record, then return that
2408         if(isset($this->record[$fieldName]) && is_object($this->record[$fieldName])) {
2409             return $this->record[$fieldName];
2410             
2411         // Special case for ID field
2412         } else if($fieldName == 'ID') {
2413             return new PrimaryKey($fieldName, $this);
2414             
2415         // General casting information for items in $db or $casting
2416         } else if($helper = $this->castingHelper($fieldName)) {
2417             $obj = Object::create_from_string($helper, $fieldName);
2418             $obj->setValue($this->$fieldName, $this->record, false);
2419             return $obj;
2420             
2421         // Special case for has_one relationships
2422         } else if(preg_match('/ID$/', $fieldName) && $this->has_one(substr($fieldName,0,-2))) {
2423             $val = (isset($this->record[$fieldName])) ? $this->record[$fieldName] : null;
2424             return DBField::create('ForeignKey', $val, $fieldName, $this);
2425             
2426         // Special case for ClassName
2427         } else if($fieldName == 'ClassName') {
2428             $val = get_class($this);
2429             return DBField::create('Varchar', $val, $fieldName, $this);
2430         }
2431     }
2432 
2433     /**
2434      * Traverses to a DBField referenced by relationships between data objects.
2435      * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
2436      *
2437      * @param $fieldPath string
2438      * @return DBField
2439      */
2440     public function relObject($fieldPath) {
2441         $parts = explode('.', $fieldPath);
2442         $fieldName = array_pop($parts);
2443         $component = $this;
2444         foreach($parts as $relation) {
2445             if ($rel = $component->has_one($relation)) {
2446                 $component = singleton($rel);
2447             } elseif ($rel = $component->has_many($relation)) {
2448                 $component = singleton($rel);
2449             } elseif ($rel = $component->many_many($relation)) {
2450                 $component = singleton($rel[1]);
2451             } elseif($className = $this->castingClass($relation)) {
2452                 $component = $className;
2453             }
2454         }
2455 
2456         $object = $component->dbObject($fieldName);
2457 
2458         if (!($object instanceof DBField) && !($object instanceof ComponentSet)) {
2459             // Todo: come up with a broader range of exception objects to describe differnet kinds of errors programatically
2460             throw new Exception("Unable to traverse to related object field [$fieldPath] on [$this->class]");
2461         }
2462         return $object;
2463     }
2464 
2465     /**
2466      * Temporary hack to return an association name, based on class, to get around the mangle
2467      * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
2468      * 
2469      * @return String
2470      */
2471     public function getReverseAssociation($className) {
2472         if (is_array($this->many_many())) {
2473             $many_many = array_flip($this->many_many());
2474             if (array_key_exists($className, $many_many)) return $many_many[$className];
2475         }
2476         if (is_array($this->has_many())) {
2477             $has_many = array_flip($this->has_many());
2478             if (array_key_exists($className, $has_many)) return $has_many[$className];
2479         }
2480         if (is_array($this->has_one())) {
2481             $has_one = array_flip($this->has_one());
2482             if (array_key_exists($className, $has_one)) return $has_one[$className];
2483         }
2484         
2485         return false;
2486     }
2487 
2488     /**
2489      * Build a {@link SQLQuery} object to perform the given query.
2490      *
2491      * @param string $filter A filter to be inserted into the WHERE clause.
2492      * @param string|array $sort A sort expression to be inserted into the ORDER BY clause. If omitted, self::$default_sort will be used.
2493      * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
2494      * @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned.
2495      * @param boolean $restictClasses Restrict results to only objects of either this class of a subclass of this class
2496      * @param string $having A filter to be inserted into the HAVING clause.
2497      *
2498      * @return SQLQuery Query built.
2499      */
2500     public function buildSQL($filter = "", $sort = "", $limit = "", $join = "", $restrictClasses = true, $having = "") {
2501         // Cache the big hairy part of buildSQL
2502         if(!isset(self::$cache_buildSQL_query[$this->class])) {
2503             // Get the tables to join to
2504             $tableClasses = ClassInfo::dataClassesFor($this->class);
2505             if(!$tableClasses) {
2506                 if(!ManifestBuilder::has_been_included()) {
2507                     user_error("DataObjects have been requested before the manifest is loaded. Please ensure you are not querying the database in _config.php.", E_USER_ERROR);
2508                 } else {
2509                     user_error("DataObject::buildSQL: Can't find data classes (classes linked to tables) for $this->class. Please ensure you run dev/build after creating a new DataObject.", E_USER_ERROR);
2510                 }
2511             }
2512 
2513             $baseClass = array_shift($tableClasses);
2514 
2515 
2516             // $collidingFields will keep a list fields that appear in mulitple places in the class
2517             // heirarchy for this table.  They will be dealt with more explicitly in the SQL query
2518             // to ensure that junk data from other tables doesn't corrupt data objects
2519             $collidingFields = array();
2520 
2521             // Build our intial query
2522             $query = new SQLQuery(array());
2523             $query->from("\"$baseClass\"");
2524 
2525             // Add SQL for multi-value fields on the base table
2526             $databaseFields = self::database_fields($baseClass);
2527             if($databaseFields) foreach($databaseFields as $k => $v) {
2528                 if(!in_array($k, array('ClassName', 'LastEdited', 'Created')) && ClassInfo::classImplements($v, 'CompositeDBField')) {
2529                     $this->dbObject($k)->addToQuery($query);
2530                 } else {
2531                     $query->select[$k] = "\"$baseClass\".\"$k\"";
2532                 }
2533             }
2534             // Join all the tables
2535             if($tableClasses && self::$subclass_access) {
2536                 foreach($tableClasses as $tableClass) {
2537                     $query->from[$tableClass] = "LEFT JOIN \"$tableClass\" ON \"$tableClass\".\"ID\" = \"$baseClass\".\"ID\"";
2538 
2539                     // Add SQL for multi-value fields
2540                     $databaseFields = self::database_fields($tableClass);
2541                     $compositeFields = self::composite_fields($tableClass, false);
2542                     if($databaseFields) foreach($databaseFields as $k => $v) {
2543                         if(!isset($compositeFields[$k])) {
2544                             // Update $collidingFields if necessary
2545                             if(isset($query->select[$k])) {
2546                                 if(!isset($collidingFields[$k])) $collidingFields[$k] = array($query->select[$k]);
2547                                 $collidingFields[$k][] = "\"$tableClass\".\"$k\"";
2548                                 
2549                             } else {
2550                                 $query->select[$k] = "\"$tableClass\".\"$k\"";
2551                             }
2552                         }
2553                     }
2554                     if($compositeFields) foreach($compositeFields as $k => $v) {
2555                         $dbO = $this->dbObject($k);
2556                         if($dbO) $dbO->addToQuery($query);
2557                     }
2558                 }
2559             }
2560             
2561             // Resolve colliding fields
2562             if($collidingFields) {
2563                 foreach($collidingFields as $k => $collisions) {
2564                     $caseClauses = array();
2565                     foreach($collisions as $collision) {
2566                         if(preg_match('/^"([^"]+)"/', $collision, $matches)) {
2567                             $collisionBase = $matches[1];
2568                             $collisionClasses = ClassInfo::subclassesFor($collisionBase);
2569                             $caseClauses[] = "WHEN \"$baseClass\".\"ClassName\" IN ('"
2570                                 . implode("', '", $collisionClasses) . "') THEN $collision";
2571                         } else {
2572                             user_error("Bad collision item '$collision'", E_USER_WARNING);
2573                         }
2574                     }
2575                     $query->select[$k] = "CASE " . implode( " ", $caseClauses) . " ELSE NULL END"
2576                         .  " AS \"$k\"";
2577                 }
2578             }
2579             
2580 
2581             $query->select[] = "\"$baseClass\".\"ID\"";
2582             $query->select[] = "CASE WHEN \"$baseClass\".\"ClassName\" IS NOT NULL THEN \"$baseClass\".\"ClassName\" ELSE '$baseClass' END AS \"RecordClassName\"";
2583 
2584             // Get the ClassName values to filter to
2585             $classNames = ClassInfo::subclassesFor($this->class);
2586 
2587             if(!$classNames) {
2588                 user_error("DataObject::get() Can't find data sub-classes for '$callerClass'");
2589             }
2590 
2591             // If querying the base class, don't bother filtering on class name
2592             if($restrictClasses && $this->class != $baseClass) {
2593                 // Get the ClassName values to filter to
2594                 $classNames = ClassInfo::subclassesFor($this->class);
2595                 if(!$classNames) {
2596                     user_error("DataObject::get() Can't find data sub-classes for '$callerClass'");
2597                 }
2598 
2599                 $query->where[] = "\"$baseClass\".\"ClassName\" IN ('" . implode("','", $classNames) . "')";
2600             }
2601             self::$cache_buildSQL_query[$this->class] = clone $query;
2602         } else {
2603             $query = clone self::$cache_buildSQL_query[$this->class];
2604             
2605         }
2606         
2607         // Find a default sort
2608         if(!$sort) {
2609             $sort = $this->stat('default_sort');
2610         }
2611         // Add quoting to sort expression if it's a simple column name
2612         if(preg_match('/^[A-Z][A-Z0-9_]*$/i', $sort)) $sort = "\"$sort\"";
2613 
2614         $query->where($filter);
2615         $query->orderby($sort);
2616         $query->limit($limit);
2617         
2618 
2619         if($having) {
2620             $query->having[] = $having;
2621         }
2622 
2623         if($join) {
2624             $query->from[] = $join;
2625             // In order to group by unique columns we have to group by everything listed in the select
2626             foreach($query->select as $field) {
2627                 // Skip the _SortColumns; these are only going to be aggregate functions
2628                 if(preg_match('/AS\s+\"?_SortColumn/', $field, $matches)) {
2629                 
2630                 // Identify columns with aliases, and ignore the alias.  Making use of the alias in
2631                 // group by was causing problems when those queries were subsequently passed into
2632                 // SQLQuery::unlimitedRowCount.
2633                 } else if(preg_match('/^(.*)\s+AS\s+(\"[^"]+\")\s*$/', $field, $matches)) {
2634                     $query->groupby[] = $matches[1];
2635                 // Otherwise just use the field as is
2636                 } else {
2637                     $query->groupby[] = $field;
2638                 }
2639             }
2640         }
2641 
2642         return $query;
2643     }
2644     
2645     /**
2646      * Cache for the hairy bit of buildSQL
2647      */
2648     private static $cache_buildSQL_query;
2649 
2650     /**
2651      * Like {@link buildSQL}, but applies the extension modifications.
2652      * 
2653      * @uses DataObjectDecorator->augmentSQL()
2654      *
2655      * @param string $filter A filter to be inserted into the WHERE clause.
2656      * @param string|array $sort A sort expression to be inserted into the ORDER BY clause. If omitted, self::$default_sort will be used.
2657      * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
2658      * @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned.
2659      * @param string $having A filter to be inserted into the HAVING clause.
2660      * @return SQLQuery Query built
2661      */
2662     public function extendedSQL($filter = "", $sort = "", $limit = "", $join = "", $having = ""){
2663         $query = $this->buildSQL($filter, $sort, $limit, $join, true, $having);
2664         $this->extend('augmentSQL', $query);
2665         return $query;
2666     }
2667 
2668     /**
2669      * Return all objects matching the filter
2670      * sub-classes are automatically selected and included
2671      *
2672      * @param string $callerClass The class of objects to be returned
2673      * @param string $filter A filter to be inserted into the WHERE clause.
2674      * @param string|array $sort A sort expression to be inserted into the ORDER BY clause.  If omitted, self::$default_sort will be used.
2675      * @param string $join A single join clause.  This can be used for filtering, only 1 instance of each DataObject will be returned.
2676      * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
2677      * @param string $containerClass The container class to return the results in.
2678      *
2679      * @return mixed The objects matching the filter, in the class specified by $containerClass
2680      */
2681     public static function get($callerClass, $filter = "", $sort = "", $join = "", $limit = "", $containerClass = "DataObjectSet") {
2682         return singleton($callerClass)->instance_get($filter, $sort, $join, $limit, $containerClass);
2683     }
2684 
2685     /**
2686      * The internal function that actually performs the querying for get().
2687      * DataObject::get("Table","filter") is the same as singleton("Table")->instance_get("filter")
2688      *
2689      * @param string $filter A filter to be inserted into the WHERE clause.
2690      * @param string $sort A sort expression to be inserted into the ORDER BY clause.  If omitted, self::$default_sort will be used.
2691      * @param string $join A single join clause.  This can be used for filtering, only 1 instance of each DataObject will be returned.
2692      * @param string $limit A limit expression to be inserted into the LIMIT clause.
2693      * @param string $containerClass The container class to return the results in.
2694      *
2695      * @return mixed The objects matching the filter, in the class specified by $containerClass
2696      */
2697     public function instance_get($filter = "", $sort = "", $join = "", $limit="", $containerClass = "DataObjectSet") {
2698         if(!DB::isActive()) {
2699             user_error("DataObjects have been requested before the database is ready. Please ensure your database connection details are correct, your database has been built, and that you are not trying to query the database in _config.php.", E_USER_ERROR);
2700         }
2701         
2702         $query = $this->extendedSQL($filter, $sort, $limit, $join);
2703         
2704         $records = $query->execute();
2705         
2706         $ret = $this->buildDataObjectSet($records, $containerClass, $query, $this->class);
2707         if($ret) $ret->parseQueryLimit($query);
2708 
2709         return $ret;
2710     }
2711 
2712     /**
2713      * Take a database {@link SS_Query} and instanciate an object for each record.
2714      *
2715      * @param SS_Query|array $records The database records, a {@link SS_Query} object or an array of maps.
2716      * @param string $containerClass The class to place all of the objects into.
2717      *
2718      * @return mixed The new objects in an object of type $containerClass
2719      */
2720     function buildDataObjectSet($records, $containerClass = "DataObjectSet", $query = null, $baseClass = null) {
2721         foreach($records as $record) {
2722             if(empty($record['RecordClassName'])) {
2723                 $record['RecordClassName'] = $record['ClassName'];
2724             }
2725             if(class_exists($record['RecordClassName'])) {
2726                 $results[] = new $record['RecordClassName']($record);
2727             } else {
2728                 if(!$baseClass) {
2729                     user_error("Bad RecordClassName '{$record['RecordClassName']}' and "
2730                         . "\$baseClass not set", E_USER_ERROR);
2731                 } else if(!is_string($baseClass) || !class_exists($baseClass)) {
2732                     user_error("Bad RecordClassName '{$record['RecordClassName']}' and bad "
2733                         . "\$baseClass '$baseClass not set", E_USER_ERROR);
2734                 }
2735                 $results[] = new $baseClass($record);
2736             }
2737         }
2738 
2739         if(isset($results)) {
2740             return new $containerClass($results);
2741         }
2742     }
2743 
2744     /**
2745      * A cache used by get_one.
2746      * @var array
2747      */
2748     protected static $cache_get_one;
2749 
2750     /**
2751      * Return the first item matching the given query.
2752      * All calls to get_one() are cached.
2753      *
2754      * @param string $callerClass The class of objects to be returned
2755      * @param string $filter A filter to be inserted into the WHERE clause
2756      * @param boolean $cache Use caching
2757      * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
2758      *
2759      * @return DataObject The first item matching the query
2760      */
2761     public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "") {
2762         $SNG = singleton($callerClass);
2763 
2764         $cacheKey = "{$filter}-{$orderby}";
2765         if($extra = $SNG->extend('cacheKeyComponent')) {
2766             $cacheKey .= '-' . implode("-", $extra);
2767         }
2768         $cacheKey = md5($cacheKey);
2769         
2770         // Flush destroyed items out of the cache
2771         if($cache && isset(DataObject::$cache_get_one[$callerClass][$cacheKey]) && DataObject::$cache_get_one[$callerClass][$cacheKey] instanceof DataObject && DataObject::$cache_get_one[$callerClass][$cacheKey]->destroyed) {
2772             DataObject::$cache_get_one[$callerClass][$cacheKey
2773             ] = false;
2774         }
2775         if(!$cache || !isset(DataObject::$cache_get_one[$callerClass][$cacheKey])) {
2776             $item = $SNG->instance_get_one($filter, $orderby);
2777             if($cache) {
2778                 DataObject::$cache_get_one[$callerClass][$cacheKey] = $item;
2779                 if(!DataObject::$cache_get_one[$callerClass][$cacheKey]) {
2780                     DataObject::$cache_get_one[$callerClass][$cacheKey] = false;
2781                 }
2782             }
2783         }
2784         return $cache ? DataObject::$cache_get_one[$callerClass][$cacheKey] : $item;
2785     }
2786 
2787     /**
2788      * Flush the cached results for all relations (has_one, has_many, many_many)
2789      * Also clears any cached aggregate data
2790      * 
2791      * @param boolean $persistant When true will also clear persistant data stored in the Cache system.
2792      *                            When false will just clear session-local cached data 
2793      * 
2794      */
2795     public function flushCache($persistant=true) {
2796         if($persistant) Aggregate::flushCache($this->class);
2797         
2798         if($this->class == 'DataObject') {
2799             DataObject::$cache_get_one = array();
2800             return;
2801         }
2802 
2803         $classes = ClassInfo::ancestry($this->class);
2804         foreach($classes as $class) {
2805             if(isset(self::$cache_get_one[$class])) unset(self::$cache_get_one[$class]);
2806         }
2807         
2808         $this->extend('flushCache');
2809         
2810         $this->componentCache = array();
2811     }
2812 
2813     static function flush_and_destroy_cache() {
2814         if(self::$cache_get_one) foreach(self::$cache_get_one as $class => $items) {
2815             if(is_array($items)) foreach($items as $item) {
2816                 if($item) $item->destroy();
2817             }
2818         }
2819         self::$cache_get_one = array();
2820     }
2821     
2822     /**
2823      * Reset internal caches, for example after test runs
2824      */
2825     static function reset() {
2826         self::$cache_get_one = array();
2827         self::$cache_buildSQL_query = array();
2828     }
2829 
2830     /**
2831      * Does the hard work for get_one()
2832      * 
2833      * @uses DataObjectDecorator->augmentSQL()
2834      *
2835      * @param string $filter A filter to be inserted into the WHERE clause
2836      * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
2837      * @return DataObject The first item matching the query
2838      */
2839     public function instance_get_one($filter, $orderby = null) {
2840         if(!DB::isActive()) {
2841             user_error("DataObjects have been requested before the database is ready. Please ensure your database connection details are correct, your database has been built, and that you are not trying to query the database in _config.php.", E_USER_ERROR);
2842         }
2843         
2844         $query = $this->buildSQL($filter);
2845         $query->limit = "1";
2846         if($orderby) {
2847             $query->orderby = $orderby;
2848         }
2849 
2850         $this->extend('augmentSQL', $query);
2851 
2852         $records = $query->execute();
2853         $records->rewind();
2854         $record = $records->current();
2855 
2856         if($record) {
2857             // Mid-upgrade, the database can have invalid RecordClassName values that need to be guarded against.
2858             if(class_exists($record['RecordClassName'])) {
2859                 $record = new $record['RecordClassName']($record);
2860             } else {
2861                 $record = new $this->class($record);
2862             }
2863 
2864             // Rather than restrict classes at the SQL-query level, we now check once the object has been instantiated
2865             // This lets us check up on weird errors where the class has been incorrectly set, and give warnings to our
2866             // developers
2867             return $record;
2868         }
2869     }
2870 
2871     /**
2872      * Return the given element, searching by ID
2873      *
2874      * @param string $callerClass The class of the object to be returned
2875      * @param int $id The id of the element
2876      * @param boolean $cache See {@link get_one()}
2877      *
2878      * @return DataObject The element
2879      */
2880     public static function get_by_id($callerClass, $id, $cache = true) {
2881         if(is_numeric($id)) {
2882             if(is_subclass_of(($callerClass), 'DataObject')) {
2883                 $tableClasses = ClassInfo::dataClassesFor($callerClass);
2884                 $baseClass = array_shift($tableClasses);
2885                 return DataObject::get_one($callerClass,"\"$baseClass\".\"ID\" = $id", $cache);
2886 
2887                 // This simpler code will be used by non-DataObject classes that implement DataObjectInterface
2888             } else {
2889                 return DataObject::get_one($callerClass,"\"ID\" = $id", $cache);
2890             }
2891         } else {
2892             user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
2893         }
2894     }
2895 
2896     /**
2897      * Get the name of the base table for this object
2898      */
2899     public function baseTable() {
2900         $tableClasses = ClassInfo::dataClassesFor($this->class);
2901         return array_shift($tableClasses);
2902     }
2903 
2904     //-------------------------------------------------------------------------------------------//
2905 
2906     /**
2907      * Return the database indexes on this table.
2908      * This array is indexed by the name of the field with the index, and
2909      * the value is the type of index.
2910      */
2911     public function databaseIndexes() {
2912         $has_one = $this->uninherited('has_one',true);
2913         $classIndexes = $this->uninherited('indexes',true);
2914         //$fileIndexes = $this->uninherited('fileIndexes', true);
2915 
2916         $indexes = array();
2917 
2918         if($has_one) {
2919             foreach($has_one as $relationshipName => $fieldType) {
2920                 $indexes[$relationshipName . 'ID'] = true;
2921             }
2922         }
2923 
2924         if($classIndexes) {
2925             foreach($classIndexes as $indexName => $indexType) {
2926                 $indexes[$indexName] = $indexType;
2927             }
2928         }
2929 
2930         if(get_parent_class($this) == "DataObject") {
2931             $indexes['ClassName'] = true;
2932         }
2933 
2934         return $indexes;
2935     }
2936 
2937     /**
2938      * Check the database schema and update it as necessary.
2939      * 
2940      * @uses DataObjectDecorator->augmentDatabase()
2941      */
2942     public function requireTable() {
2943         // Only build the table if we've actually got fields
2944         $fields = self::database_fields($this->class);
2945         $extensions = self::database_extensions($this->class);
2946         
2947         $indexes = $this->databaseIndexes();
2948 
2949         if($fields) {
2950             $hasAutoIncPK = ($this->class == ClassInfo::baseDataClass($this->class));
2951             DB::requireTable($this->class, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'), $extensions);
2952         } else {
2953             DB::dontRequireTable($this->class);
2954         }
2955 
2956         // Build any child tables for many_many items
2957         if($manyMany = $this->uninherited('many_many', true)) {
2958             $extras = $this->uninherited('many_many_extraFields', true);
2959             foreach($manyMany as $relationship => $childClass) {
2960                 // Build field list
2961                 $manymanyFields = array(
2962                     "{$this->class}ID" => "Int",
2963                 (($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => "Int",
2964                 );
2965                 if(isset($extras[$relationship])) {
2966                     $manymanyFields = array_merge($manymanyFields, $extras[$relationship]);
2967                 }
2968 
2969                 // Build index list
2970                 $manymanyIndexes = array(
2971                     "{$this->class}ID" => true,
2972                 (($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => true,
2973                 );
2974                 
2975                 DB::requireTable("{$this->class}_$relationship", $manymanyFields, $manymanyIndexes, true, null, $extensions);
2976             }
2977         }
2978 
2979         // Let any extentions make their own database fields
2980         $this->extend('augmentDatabase', $dummy);
2981     }
2982 
2983     /**
2984      * Add default records to database. This function is called whenever the
2985      * database is built, after the database tables have all been created. Overload
2986      * this to add default records when the database is built, but make sure you
2987      * call parent::requireDefaultRecords().
2988      * 
2989      * @uses DataObjectDecorator->requireDefaultRecords()
2990      */
2991     public function requireDefaultRecords() {
2992         $defaultRecords = $this->stat('default_records');
2993 
2994         if(!empty($defaultRecords)) {
2995             $hasData = DataObject::get_one($this->class);
2996             if(!$hasData) {
2997                 $className = $this->class;
2998                 foreach($defaultRecords as $record) {
2999                     $obj = new $className($record);
3000                     $obj->write();
3001                 }
3002                 DB::alteration_message("Added default records to $className table","created");
3003             }
3004         }
3005         
3006         // Let any extentions make their own database default data
3007         $this->extend('requireDefaultRecords', $dummy);
3008     }
3009     
3010     /**
3011      * @see DataObject::database_fields()
3012      */
3013     public function databaseFields() {
3014         user_error("databaseFields() is deprecated; use self::database_fields() "
3015             . "instead", E_USER_NOTICE);
3016         return self::database_fields($this->class);
3017     }
3018     
3019     /**
3020      * @see DataObject::custom_database_fields()
3021      */
3022     public function customDatabaseFields() {
3023         user_error("customDatabaseFields() is deprecated; use self::custom_database_fields() "
3024             . "instead", E_USER_NOTICE);
3025         return self::custom_database_fields($this->class);
3026     }
3027     
3028     /**
3029      * Returns fields bu traversing the class heirachy in a bottom-up direction.
3030      *
3031      * Needed to avoid getCMSFields being empty when customDatabaseFields overlooks
3032      * the inheritance chain of the $db array, where a child data object has no $db array,
3033      * but still needs to know the properties of its parent. This should be merged into databaseFields or
3034      * customDatabaseFields.
3035      *
3036      * @todo review whether this is still needed after recent API changes
3037      */
3038     public function inheritedDatabaseFields() {
3039         $fields     = array();
3040         $currentObj = $this->class;
3041         
3042         while($currentObj != 'DataObject') {
3043             $fields     = array_merge($fields, self::custom_database_fields($currentObj));
3044             $currentObj = get_parent_class($currentObj);
3045         }
3046         
3047         return (array) $fields;
3048     }
3049 
3050     /**
3051      * Get the default searchable fields for this object,
3052      * as defined in the $searchable_fields list. If searchable
3053      * fields are not defined on the data object, uses a default
3054      * selection of summary fields.
3055      *
3056      * @return array
3057      */
3058     public function searchableFields() {
3059         // can have mixed format, need to make consistent in most verbose form
3060         $fields = $this->stat('searchable_fields');
3061         
3062         $labels = $this->fieldLabels();
3063         
3064         // fallback to summary fields
3065         if(!$fields) $fields = array_keys($this->summaryFields());
3066         
3067         // we need to make sure the format is unified before
3068         // augmenting fields, so decorators can apply consistent checks
3069         // but also after augmenting fields, because the decorator
3070         // might use the shorthand notation as well
3071 
3072         // rewrite array, if it is using shorthand syntax
3073         $rewrite = array();
3074         foreach($fields as $name => $specOrName) {
3075             $identifer = (is_int($name)) ? $specOrName : $name;
3076 
3077             if(is_int($name)) {
3078                 // Format: array('MyFieldName')
3079                 $rewrite[$identifer] = array();
3080             } elseif(is_array($specOrName)) {
3081                 // Format: array('MyFieldName' => array(
3082                 //   'filter => 'ExactMatchFilter',
3083                 //   'field' => 'NumericField', // optional
3084                 //   'title' => 'My Title', // optiona.
3085                 // ))
3086                 $rewrite[$identifer] = array_merge(
3087                     array('filter' => $this->relObject($identifer)->stat('default_search_filter_class')),
3088                     (array)$specOrName
3089                 );
3090             } else {
3091                 // Format: array('MyFieldName' => 'ExactMatchFilter')
3092                 $rewrite[$identifer] = array(
3093                     'filter' => $specOrName,
3094                 );
3095             }
3096             if(!isset($rewrite[$identifer]['title'])) {
3097                 $rewrite[$identifer]['title'] = (isset($labels[$identifer])) ? $labels[$identifer] : FormField::name_to_label($identifer);
3098             }
3099             if(!isset($rewrite[$identifer]['filter'])) {
3100                 $rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3101             }
3102         }
3103 
3104         $fields = $rewrite;
3105         
3106         // apply DataObjectDecorators if present
3107         $this->extend('updateSearchableFields', $fields);
3108 
3109         return $fields;
3110     }
3111     
3112     /**
3113      * Get any user defined searchable fields labels that
3114      * exist. Allows overriding of default field names in the form
3115      * interface actually presented to the user.
3116      *
3117      * The reason for keeping this separate from searchable_fields,
3118      * which would be a logical place for this functionality, is to
3119      * avoid bloating and complicating the configuration array. Currently
3120      * much of this system is based on sensible defaults, and this property
3121      * would generally only be set in the case of more complex relationships
3122      * between data object being required in the search interface.
3123      *
3124      * Generates labels based on name of the field itself, if no static property 
3125      * {@link self::field_labels} exists.
3126      *
3127      * @uses $field_labels
3128      * @uses FormField::name_to_label()
3129      *
3130      * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3131      * 
3132      * @return array|string Array of all element labels if no argument given, otherwise the label of the field
3133      */
3134     public function fieldLabels($includerelations = true) {
3135         $customLabels = $this->stat('field_labels');
3136         $autoLabels = array();
3137         
3138         // get all translated static properties as defined in i18nCollectStatics()
3139         $ancestry = ClassInfo::ancestry($this->class);
3140         $ancestry = array_reverse($ancestry);
3141         if($ancestry) foreach($ancestry as $ancestorClass) {
3142             if($ancestorClass == 'ViewableData') break;
3143             $types = array();
3144             $types['db'] = array_merge(
3145                 array('ClassName'=>1, 'Created'=>1, 'LastEdited'=>1),
3146                 (array) Object::uninherited_static($ancestorClass, 'db')
3147             );
3148             if($includerelations){
3149                 $types['has_one'] = (array)singleton($ancestorClass)->uninherited('has_one', true);
3150                 $types['has_many'] = (array)singleton($ancestorClass)->uninherited('has_many', true);
3151                 $types['many_many'] = (array)singleton($ancestorClass)->uninherited('many_many', true);
3152             }
3153             foreach($types as $type => $attrs) {
3154                 foreach($attrs as $name => $spec) {
3155                     $label = FormField::name_to_label($name);
3156                     if (!isset($autoLabels[$name]) || $autoLabels[$name] == $label || preg_match("/{$type}_{$name}/", $autoLabels[$name]))
3157                         $autoLabels[$name] = _t("{$ancestorClass}.{$type}_{$name}", $label);
3158                 }
3159             }
3160 
3161         }
3162 
3163         $labels = array_merge((array)$autoLabels, (array)$customLabels);
3164         
3165         $this->extend('updateFieldLabels', $labels);
3166 
3167         return $labels;
3168     }
3169     
3170     /**
3171      * Get a human-readable label for a single field,
3172      * see {@link fieldLabels()} for more details.
3173      * 
3174      * @uses fieldLabels()
3175      * @uses FormField::name_to_label()
3176      * 
3177      * @param string $name Name of the field
3178      * @return string Label of the field
3179      */
3180     public function fieldLabel($name) {
3181         $labels = $this->fieldLabels();
3182         return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
3183     }
3184 
3185     /**
3186      * Get the default summary fields for this object.
3187      *
3188      * @todo use the translation apparatus to return a default field selection for the language
3189      *
3190      * @return array
3191      */
3192     public function summaryFields(){
3193 
3194         $fields = $this->stat('summary_fields');
3195 
3196         // if fields were passed in numeric array,
3197         // convert to an associative array
3198         if($fields && array_key_exists(0, $fields)) {
3199             $localized = array();
3200             foreach ($fields as $name) {
3201                 $localized[$name] = $this->fieldLabel($name);
3202             }
3203             $fields = $localized;
3204         }
3205 
3206         if (!$fields) {
3207             $fields = array();
3208             // try to scaffold a couple of usual suspects
3209             if ($this->hasField('Name')) $fields['Name'] = $this->fieldLabel('Name');
3210             if ($this->hasDataBaseField('Title')) $fields['Title'] = $this->fieldLabel('Title');
3211             if ($this->hasField('Description')) $fields['Description'] = $this->fieldLabel('Description');
3212             if ($this->hasField('FirstName')) $fields['FirstName'] = $this->fieldLabel('FirstName');
3213         }
3214         $this->extend("updateSummaryFields", $fields);
3215         
3216         // Final fail-over, just list ID field
3217         if(!$fields) $fields['ID'] = 'ID';
3218         
3219         return $fields;
3220     }
3221 
3222     /**
3223      * Defines a default list of filters for the search context.
3224      *
3225      * If a filter class mapping is defined on the data object,
3226      * it is constructed here. Otherwise, the default filter specified in
3227      * {@link DBField} is used.
3228      *
3229      * @todo error handling/type checking for valid FormField and SearchFilter subclasses?
3230      *
3231      * @return array
3232      */
3233     public function defaultSearchFilters() {
3234         $filters = array();
3235         foreach($this->searchableFields() as $name => $spec) {
3236             $filterClass = $spec['filter'];
3237             // if $filterClass is not set a name of any subclass of SearchFilter than assing 'PartiailMatchFilter' to it
3238             if (!is_subclass_of($filterClass, 'SearchFilter')) {
3239                 $filterClass = 'PartialMatchFilter';
3240             }
3241             $filters[$name] = new $filterClass($name);
3242         }
3243         return $filters;
3244     }
3245 
3246     /**
3247      * @return boolean True if the object is in the database
3248      */
3249     public function isInDB() {
3250         return is_numeric( $this->ID ) && $this->ID > 0;
3251     }
3252 
3253     /**
3254      * Sets a 'context object' that can be used to provide hints about how to process a particular get / get_one request.
3255      * In particular, DataObjectDecorators can use this to amend queries more effectively.
3256      * Care must be taken to unset the context object after you're done with it, otherwise you will have a stale context,
3257      * which could cause horrible bugs.
3258      */
3259     public static function set_context_obj($obj) {
3260         if($obj && self::$context_obj) user_error("Dataobject::set_context_obj passed " . $obj->class . "." . $obj->ID . " when there is already a context: " . self::$context_obj->class . '.' . self::$context_obj->ID, E_USER_WARNING);
3261         self::$context_obj = $obj;
3262     }
3263 
3264     /**
3265      * Retrieve the current context object.
3266      */
3267     public static function context_obj() {
3268         return self::$context_obj;
3269     }
3270 
3271     /**
3272      * @ignore
3273      */
3274     protected static $context_obj = null;
3275 
3276     /*
3277      * @ignore
3278      */
3279     private static $subclass_access = true; 
3280     
3281     /**
3282      * Temporarily disable subclass access in data object qeur
3283      */
3284     static function disable_subclass_access() {
3285         self::$subclass_access = false;
3286     }
3287     static function enable_subclass_access() {
3288         self::$subclass_access = true;
3289     }
3290     
3291     //-------------------------------------------------------------------------------------------//
3292 
3293     /**
3294      * Database field definitions.
3295      * This is a map from field names to field type. The field
3296      * type should be a class that extends .
3297      * @var array
3298      */
3299     public static $db = null;
3300 
3301     /**
3302      * Use a casting object for a field. This is a map from
3303      * field name to class name of the casting object.
3304      * @var array
3305      */
3306     public static $casting = array(
3307         "LastEdited" => "SS_Datetime",
3308         "Created" => "SS_Datetime",
3309         "Title" => 'Text',
3310     );
3311     
3312     /**
3313      * Specify custom options for a CREATE TABLE call.
3314      * Can be used to specify a custom storage engine for specific database table.
3315      * All options have to be keyed for a specific database implementation,
3316      * identified by their class name (extending from {@link SS_Database}).
3317      * 
3318      * <code>
3319      * array(
3320      *  'MySQLDatabase' => 'ENGINE=MyISAM'
3321      * )
3322      * </code>
3323      *
3324      * Caution: This API is experimental, and might not be
3325      * included in the next major release. Please use with care.
3326      * 
3327      * @var array
3328      */
3329     static $create_table_options = array(
3330         'MySQLDatabase' => 'ENGINE=MyISAM'
3331     );
3332 
3333     /**
3334      * If a field is in this array, then create a database index
3335      * on that field. This is a map from fieldname to index type.
3336      * See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation.
3337      * 
3338      * @var array
3339      */
3340     public static $indexes = null;
3341 
3342     /**
3343      * Inserts standard column-values when a DataObject
3344      * is instanciated. Does not insert default records {@see $default_records}.
3345      * This is a map from fieldname to default value.
3346      * 
3347      *  - If you would like to change a default value in a sub-class, just specify it.
3348      *  - If you would like to disable the default value given by a parent class, set the default value to 0,'',or false in your
3349      *    subclass.  Setting it to null won't work.
3350      * 
3351      * @var array
3352      */
3353     public static $defaults = null;
3354 
3355     /**
3356      * Multidimensional array which inserts default data into the database
3357      * on a db/build-call as long as the database-table is empty. Please use this only
3358      * for simple constructs, not for SiteTree-Objects etc. which need special
3359      * behaviour such as publishing and ParentNodes.
3360      *
3361      * Example:
3362      * array(
3363      *  array('Title' => "DefaultPage1", 'PageTitle' => 'page1'),
3364      *  array('Title' => "DefaultPage2")
3365      * ).
3366      *
3367      * @var array
3368      */
3369     public static $default_records = null;
3370 
3371     /**
3372      * One-to-zero relationship defintion. This is a map of component name to data type. In order to turn this into a
3373      * true one-to-one relationship you can add a {@link DataObject::$belongs_to} relationship on the child class.
3374      *
3375      * Note that you cannot have a has_one and belongs_to relationship with the same name.
3376      *
3377      *  @var array
3378      */
3379     public static $has_one = null;
3380     
3381     /**
3382      * A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}.
3383      *
3384      * This does not actually create any data structures, but allows you to query the other object in a one-to-one
3385      * relationship from the child object. If you have multiple belongs_to links to another object you can use the
3386      * syntax "ClassName.HasOneName" to specify which foreign has_one key on the other object to use.
3387      *
3388      * Note that you cannot have a has_one and belongs_to relationship with the same name.
3389      *
3390      * @var array
3391      */
3392     public static $belongs_to;
3393     
3394     /**
3395      * This defines a one-to-many relationship. It is a map of component name to the remote data class.
3396      *
3397      * This relationship type does not actually create a data structure itself - you need to define a matching $has_one
3398      * relationship on the child class. Also, if the $has_one relationship on the child class has multiple links to this
3399      * class you can use the syntax "ClassName.HasOneRelationshipName" in the remote data class definition to show
3400      * which foreign key to use.
3401      *
3402      * @var array
3403      */
3404     public static $has_many = null;
3405 
3406     /**
3407      * many-many relationship definitions.
3408      * This is a map from component name to data type.
3409      * @var array
3410      */
3411     public static $many_many = null;
3412 
3413     /**
3414      * Extra fields to include on the connecting many-many table.
3415      * This is a map from field name to field type.
3416      * 
3417      * Example code:
3418      * <code>
3419      * public static $many_many_extraFields = array(
3420      *  'Members' => array(
3421      *          'Role' => 'Varchar(100)'
3422      *      )
3423      * );
3424      * </code>
3425      * 
3426      * @var array
3427      */
3428     public static $many_many_extraFields = null;
3429 
3430     /**
3431      * The inverse side of a many-many relationship.
3432      * This is a map from component name to data type.
3433      * @var array
3434      */
3435     public static $belongs_many_many = null;
3436 
3437     /**
3438      * The default sort expression. This will be inserted in the ORDER BY
3439      * clause of a SQL query if no other sort expression is provided.
3440      * @var string
3441      */
3442     public static $default_sort = null;
3443 
3444     /**
3445      * Default list of fields that can be scaffolded by the ModelAdmin
3446      * search interface.
3447      *
3448      * Overriding the default filter, with a custom defined filter:
3449      * <code>
3450      *  static $searchable_fields = array(
3451      *     "Name" => "PartialMatchFilter"
3452      *  );
3453      * </code>
3454      * 
3455      * Overriding the default form fields, with a custom defined field.
3456      * The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
3457      * The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
3458      * <code>
3459      *  static $searchable_fields = array(
3460      *     "Name" => array(
3461      *          "field" => "TextField"
3462      *      )
3463      *  );
3464      * </code>
3465      *
3466      * Overriding the default form field, filter and title:
3467      * <code>
3468      *  static $searchable_fields = array(
3469      *     "Organisation.ZipCode" => array(
3470      *          "field" => "TextField", 
3471      *          "filter" => "PartialMatchFilter",
3472      *          "title" => 'Organisation ZIP'
3473      *      )
3474      *  );
3475      * </code>
3476      */
3477     public static $searchable_fields = null;
3478 
3479     /**
3480      * User defined labels for searchable_fields, used to override
3481      * default display in the search form.
3482      */
3483     public static $field_labels = null;
3484 
3485     /**
3486      * Provides a default list of fields to be used by a 'summary'
3487      * view of this object.
3488      */
3489     public static $summary_fields = null;
3490     
3491     /**
3492      * Provides a list of allowed methods that can be called via RESTful api.
3493      */
3494     public static $allowed_actions = null;
3495     
3496     /**
3497      * Collect all static properties on the object
3498      * which contain natural language, and need to be translated.
3499      * The full entity name is composed from the class name and a custom identifier.
3500      * 
3501      * @return array A numerical array which contains one or more entities in array-form.
3502      * Each numeric entity array contains the "arguments" for a _t() call as array values:
3503      * $entity, $string, $priority, $context.
3504      */
3505     public function provideI18nEntities() {
3506         $entities = array();
3507         
3508         $entities["{$this->class}.SINGULARNAME"] = array(
3509             $this->singular_name(),
3510             PR_MEDIUM,
3511             'Singular name of the object, used in dropdowns and to generally identify a single object in the interface'
3512         );
3513 
3514         $entities["{$this->class}.PLURALNAME"] = array(
3515             $this->plural_name(),
3516             PR_MEDIUM,
3517             'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the interface'
3518         );
3519         
3520         return $entities;
3521     }
3522     
3523     /**
3524      * Returns true if the given method/parameter has a value
3525      * (Uses the DBField::hasValue if the parameter is a database field)
3526      * 
3527      * @param string $field The field name
3528      * @param array $arguments
3529      * @param bool $cache
3530      * @return boolean
3531      */
3532     function hasValue($field, $arguments = null, $cache = true) {
3533         $obj = $this->dbObject($field);
3534         if($obj) {
3535             return $obj->hasValue();
3536         } else {
3537             return parent::hasValue($field, $arguments, $cache);
3538         }
3539     }
3540 
3541 }
[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -
Webylon 3.1 API Docs API documentation generated by ApiGen 2.8.0