2007-07-19 12:40:28 +02:00
< ? php
/**
* A single database record & abstract class for the data - access - model .
2008-09-22 18:02:03 +02:00
*
2011-04-15 11:35:30 +02:00
* < h2 > Extensions </ h2 >
2009-03-31 21:34:37 +02:00
*
2011-04-15 11:35:30 +02:00
* See { @ link Extension } and { @ link DataExtension } .
2009-03-31 21:34:37 +02:00
*
* < h2 > Permission Control </ h2 >
*
2008-09-22 18:02:03 +02:00
* Object - level access control by { @ link Permission } . Permission codes are arbitrary
* strings which can be selected on a group - by - group basis .
*
* < code >
* class Article extends DataObject implements PermissionProvider {
* static $api_access = true ;
*
2012-09-19 12:07:39 +02:00
* function canView ( $member = false ) {
2008-09-22 18:02:03 +02:00
* return Permission :: check ( 'ARTICLE_VIEW' );
* }
2012-09-19 12:07:39 +02:00
* function canEdit ( $member = false ) {
2008-09-22 18:02:03 +02:00
* return Permission :: check ( 'ARTICLE_EDIT' );
* }
2012-09-19 12:07:39 +02:00
* function canDelete () {
2008-09-22 18:02:03 +02:00
* return Permission :: check ( 'ARTICLE_DELETE' );
* }
2012-09-19 12:07:39 +02:00
* function canCreate () {
2008-09-22 18:02:03 +02:00
* return Permission :: check ( 'ARTICLE_CREATE' );
* }
2012-09-19 12:07:39 +02:00
* function providePermissions () {
2008-09-22 18:02:03 +02:00
* return array (
* 'ARTICLE_VIEW' => 'Read an article object' ,
* 'ARTICLE_EDIT' => 'Edit an article object' ,
* 'ARTICLE_DELETE' => 'Delete an article object' ,
* 'ARTICLE_CREATE' => 'Create an article object' ,
* );
* }
* }
* </ code >
*
* Object - level access control by { @ link Group } membership :
* < code >
* class Article extends DataObject {
* static $api_access = true ;
*
2012-09-19 12:07:39 +02:00
* function canView ( $member = false ) {
2008-09-22 18:02:03 +02:00
* if ( ! $member ) $member = Member :: currentUser ();
* return $member -> inGroup ( 'Subscribers' );
* }
2012-09-19 12:07:39 +02:00
* function canEdit ( $member = false ) {
2008-09-22 18:02:03 +02:00
* if ( ! $member ) $member = Member :: currentUser ();
* return $member -> inGroup ( 'Editors' );
* }
2008-10-02 15:58:08 +02:00
*
* // ...
2008-09-22 18:02:03 +02:00
* }
* </ code >
*
2009-03-17 21:20:43 +01:00
* If any public method on this class is prefixed with an underscore ,
* the results are cached in memory through { @ link cachedCall ()} .
*
2009-03-31 21:34:37 +02:00
*
* @ todo Add instance specific removeExtension () which undos loadExtraStatics ()
* and defineMethods ()
*
2012-04-12 08:02:46 +02:00
* @ package framework
2008-02-25 03:10:37 +01:00
* @ subpackage model
2007-07-19 12:40:28 +02:00
*/
2009-03-14 01:16:32 +01:00
class DataObject extends ViewableData implements DataObjectInterface , i18nEntityProvider {
/**
* Human - readable singular name .
* @ var string
*/
public static $singular_name = null ;
/**
* Human - readable pluaral name
* @ var string
*/
public static $plural_name = null ;
/**
* Allow API access to this object ?
* @ todo Define the options that can be set here
*/
public static $api_access = false ;
2012-04-12 07:03:18 +02:00
2009-03-14 01:16:32 +01:00
/**
* True if this DataObject has been destroyed .
* @ var boolean
*/
public $destroyed = false ;
2011-05-01 07:33:02 +02:00
/**
* The DataModel from this this object comes
*/
protected $model ;
2007-07-19 12:40:28 +02:00
/**
2010-12-06 22:46:24 +01:00
* Data stored in this objects database record . An array indexed by fieldname .
*
* Use { @ link toMap ()} if you want an array representation
* of this object , as the $record array might contain lazy loaded field aliases .
*
2007-07-19 12:40:28 +02:00
* @ var array
*/
protected $record ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* An array indexed by fieldname , true if the field has been changed .
2009-05-27 02:09:23 +02:00
* Use { @ link getChangedFields ()} and { @ link isChanged ()} to inspect
* the changed state .
*
2007-07-19 12:40:28 +02:00
* @ var array
*/
2009-05-27 02:09:23 +02:00
private $changed ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* The database record ( in the same format as $record ), before
* any changes .
* @ var array
*/
protected $original ;
2009-05-12 03:58:45 +02:00
/**
* Used by onBeforeDelete () to ensure child classes call parent :: onBeforeDelete ()
* @ var boolean
*/
protected $brokenOnDelete = false ;
/**
* Used by onBeforeWrite () to ensure child classes call parent :: onBeforeWrite ()
* @ var boolean
*/
protected $brokenOnWrite = false ;
2009-03-04 04:44:11 +01:00
/**
* Should dataobjects be validated before they are written ?
*/
private static $validation_enabled = true ;
2012-04-12 07:03:18 +02:00
/**
* Static caches used by relevant functions .
*/
public static $cache_has_own_table = array ();
public static $cache_has_own_table_field = array ();
protected static $_cache_get_one ;
protected static $_cache_get_class_ancestry ;
protected static $_cache_composite_fields = array ();
2012-04-13 18:38:23 +02:00
protected static $_cache_field_labels = array ();
2012-04-12 07:03:18 +02:00
/**
* Non - static relationship cache , indexed by component name .
*/
protected $components ;
2009-03-04 04:44:11 +01:00
/**
* Returns when validation on DataObjects is enabled .
* @ return bool
*/
2012-09-19 12:07:39 +02:00
public static function get_validation_enabled () {
2009-03-04 04:44:11 +01:00
return self :: $validation_enabled ;
}
/**
* Set whether DataObjects should be validated before they are written .
* @ param $enable bool
* @ see DataObject :: validate ()
*/
2012-09-19 12:07:39 +02:00
public static function set_validation_enabled ( $enable ) {
2009-03-04 04:44:11 +01:00
self :: $validation_enabled = ( bool ) $enable ;
}
2009-03-14 01:16:32 +01:00
2012-09-12 01:14:59 +02:00
/**
* @ var [ string ] - class = > ClassName field definition cache for self :: database_fields
*/
private static $classname_spec_cache = array ();
2009-03-14 01:16:32 +01:00
/**
2010-12-06 22:46:24 +01:00
* Return the complete map of fields on this object , including " Created " , " LastEdited " and " ClassName " .
* See { @ link custom_database_fields ()} for a getter that excludes these " base fields " .
2009-03-14 01:16:32 +01:00
*
* @ param string $class
* @ return array
*/
public static function database_fields ( $class ) {
if ( get_parent_class ( $class ) == 'DataObject' ) {
2012-09-12 01:14:59 +02:00
if ( ! isset ( self :: $classname_spec_cache [ $class ])) {
$classNames = ClassInfo :: subclassesFor ( $class );
$db = DB :: getConn ();
if ( $db -> hasField ( $class , 'ClassName' )) {
$existing = $db -> query ( " SELECT DISTINCT \" ClassName \" FROM \" $class\ " " )->column();
$classNames = array_unique ( array_merge ( $existing , $classNames ));
}
self :: $classname_spec_cache [ $class ] = " Enum(' " . implode ( ', ' , $classNames ) . " ') " ;
}
2012-08-29 04:30:10 +02:00
2009-03-14 01:16:32 +01:00
return array_merge (
array (
2012-09-12 01:14:59 +02:00
'ClassName' => self :: $classname_spec_cache [ $class ],
API CHANGE: Renamed conflicting classes to have an "SS_" namespace, and renamed existing "SS" namespace to "SS_". The affected classes are: HTTPRequest, HTTPResponse, Query, Database, SSBacktrace, SSCli, SSDatetime, SSDatetimeTest, SSLog, SSLogTest, SSLogEmailWriter, SSLogErrorEmailFormatter, SSLogErrorFileFormatter, SSLogFileWriter and SSZendLog.
MINOR: Replaced usage of renamed classes with the new namespaced name.
From: Andrew Short <andrewjshort@gmail.com>
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@90075 467b73ca-7a2a-4603-9d3b-597d59a354a9
2009-10-26 04:06:31 +01:00
'Created' => 'SS_Datetime' ,
'LastEdited' => 'SS_Datetime'
2009-03-14 01:16:32 +01:00
),
self :: custom_database_fields ( $class )
);
}
return self :: custom_database_fields ( $class );
}
/**
2009-06-02 05:43:45 +02:00
* Get all database columns explicitly defined on a class in { @ link DataObject :: $db }
* and { @ link DataObject :: $has_one } . Resolves instances of { @ link CompositeDBField }
* into the actual database fields , rather than the name of the field which
* might not equate a database column .
*
2010-12-06 22:46:24 +01:00
* Does not include " base fields " like " ID " , " ClassName " , " Created " , " LastEdited " ,
* see { @ link database_fields ()} .
*
2009-06-02 05:43:45 +02:00
* @ uses CompositeDBField -> compositeDatabaseFields ()
2009-03-14 01:16:32 +01:00
*
* @ param string $class
2009-06-02 05:43:45 +02:00
* @ return array Map of fieldname to specification , similiar to { @ link DataObject :: $db } .
2009-03-14 01:16:32 +01:00
*/
public static function custom_database_fields ( $class ) {
2012-04-12 18:26:56 +02:00
$fields = Config :: inst () -> get ( $class , 'db' , Config :: UNINHERITED );
2009-06-02 05:43:45 +02:00
2009-08-11 10:49:52 +02:00
foreach ( self :: composite_fields ( $class , false ) as $fieldName => $fieldClass ) {
2012-04-20 00:08:17 +02:00
// Remove the original fieldname, it's not an actual database column
2009-08-11 10:49:52 +02:00
unset ( $fields [ $fieldName ]);
// Add all composite columns
$compositeFields = singleton ( $fieldClass ) -> compositeDatabaseFields ();
if ( $compositeFields ) foreach ( $compositeFields as $compositeName => $spec ) {
$fields [ " { $fieldName } { $compositeName } " ] = $spec ;
2009-06-02 05:43:45 +02:00
}
}
2009-08-11 10:49:52 +02:00
2009-06-02 05:43:45 +02:00
// Add has_one relationships
2012-04-12 18:26:56 +02:00
$hasOne = Config :: inst () -> get ( $class , 'has_one' , Config :: UNINHERITED );
2009-03-14 01:16:32 +01:00
if ( $hasOne ) foreach ( array_keys ( $hasOne ) as $field ) {
$fields [ $field . 'ID' ] = 'ForeignKey' ;
}
2009-07-31 07:41:59 +02:00
return ( array ) $fields ;
2009-03-14 01:16:32 +01:00
}
2009-03-04 04:44:11 +01:00
2009-08-11 10:49:52 +02:00
/**
* Returns the field class if the given db field on the class is a composite field .
* Will check all applicable ancestor classes and aggregate results .
*/
2012-09-19 12:07:39 +02:00
public static function is_composite_field ( $class , $name , $aggregated = true ) {
2012-04-12 06:14:05 +02:00
if ( ! isset ( DataObject :: $_cache_composite_fields [ $class ])) self :: cache_composite_fields ( $class );
2009-08-11 10:49:52 +02:00
2012-04-12 06:14:05 +02:00
if ( isset ( DataObject :: $_cache_composite_fields [ $class ][ $name ])) {
return DataObject :: $_cache_composite_fields [ $class ][ $name ];
2009-08-11 10:49:52 +02:00
} else if ( $aggregated && $class != 'DataObject' && ( $parentClass = get_parent_class ( $class )) != 'DataObject' ) {
return self :: is_composite_field ( $parentClass , $name );
}
}
/**
* Returns a list of all the composite if the given db field on the class is a composite field .
* Will check all applicable ancestor classes and aggregate results .
*/
2012-09-19 12:07:39 +02:00
public static function composite_fields ( $class , $aggregated = true ) {
2012-04-12 06:14:05 +02:00
if ( ! isset ( DataObject :: $_cache_composite_fields [ $class ])) self :: cache_composite_fields ( $class );
2009-08-11 10:49:52 +02:00
2012-04-12 06:14:05 +02:00
$compositeFields = DataObject :: $_cache_composite_fields [ $class ];
2009-08-11 10:49:52 +02:00
if ( $aggregated && $class != 'DataObject' && ( $parentClass = get_parent_class ( $class )) != 'DataObject' ) {
$compositeFields = array_merge ( $compositeFields ,
self :: composite_fields ( $parentClass ));
}
return $compositeFields ;
}
/**
* Internal cacher for the composite field information
*/
private static function cache_composite_fields ( $class ) {
$compositeFields = array ();
2012-04-12 18:26:56 +02:00
$fields = Config :: inst () -> get ( $class , 'db' , Config :: UNINHERITED );
2009-08-11 10:49:52 +02:00
if ( $fields ) foreach ( $fields as $fieldName => $fieldClass ) {
2012-04-20 05:24:17 +02:00
if ( ! is_string ( $fieldClass )) continue ;
2009-08-11 10:49:52 +02:00
// Strip off any parameters
$bPos = strpos ( '(' , $fieldClass );
if ( $bPos !== FALSE ) $fieldClass = substr ( 0 , $bPos , $fieldClass );
// Test to see if it implements CompositeDBField
if ( ClassInfo :: classImplements ( $fieldClass , 'CompositeDBField' )) {
$compositeFields [ $fieldName ] = $fieldClass ;
}
}
2012-04-12 06:14:05 +02:00
DataObject :: $_cache_composite_fields [ $class ] = $compositeFields ;
2009-08-11 10:49:52 +02:00
}
2007-07-19 12:40:28 +02:00
/**
* Construct a new DataObject .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param array | null $record This will be null for a new database record . Alternatively , you can pass an array of
* field values . Normally this contructor is only used by the internal systems that get objects from the database .
* @ param boolean $isSingleton This this to true if this is a singleton () object , a stub for calling methods . Singletons
* don ' t have their defaults set .
*/
2012-09-19 12:07:39 +02:00
public function __construct ( $record = null , $isSingleton = false , $model = null ) {
2012-07-02 07:25:14 +02:00
parent :: __construct ();
2007-07-19 12:40:28 +02:00
// Set the fields data.
if ( ! $record ) {
2009-04-27 03:36:15 +02:00
$record = array (
'ID' => 0 ,
'ClassName' => get_class ( $this ),
'RecordClassName' => get_class ( $this )
);
2007-07-19 12:40:28 +02:00
}
if ( ! is_array ( $record )) {
if ( is_object ( $record )) $passed = " an object of type ' $record->class ' " ;
else $passed = " The value ' $record ' " ;
2007-09-15 01:10:25 +02:00
user_error ( " DataObject::__construct passed $passed . It's supposed to be passed an array,
2012-03-19 03:27:52 +01:00
taken straight from the database . Perhaps you should use DataList :: create () -> First (); instead ? " , E_USER_WARNING);
2007-07-19 12:40:28 +02:00
$record = null ;
}
2009-05-07 08:00:50 +02:00
// Set $this->record to $record, but ignore NULLs
$this -> record = array ();
foreach ( $record as $k => $v ) {
2010-10-13 03:19:33 +02:00
// Ensure that ID is stored as a number and not a string
// To do: this kind of clean-up should be done on all numeric fields, in some relatively
// performant manner
if ( $v !== null ) {
if ( $k == 'ID' && is_numeric ( $v )) $this -> record [ $k ] = ( int ) $v ;
else $this -> record [ $k ] = $v ;
}
2009-05-07 08:00:50 +02:00
}
2012-04-20 00:08:17 +02:00
// Identify fields that should be lazy loaded, but only on existing records
if ( ! empty ( $record [ 'ID' ])) {
$currentObj = get_class ( $this );
while ( $currentObj != 'DataObject' ) {
$fields = self :: custom_database_fields ( $currentObj );
foreach ( $fields as $field => $type ) {
if ( ! array_key_exists ( $field , $record )) $this -> record [ $field . '_Lazy' ] = $currentObj ;
}
$currentObj = get_parent_class ( $currentObj );
}
}
2009-05-07 08:00:50 +02:00
$this -> original = $this -> record ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
// Keep track of the modification date of all the data sourced to make this page
// From this we create a Last-Modified HTTP header
if ( isset ( $record [ 'LastEdited' ])) {
HTTP :: register_modification_date ( $record [ 'LastEdited' ]);
}
// Must be called after parent constructor
2008-02-25 03:10:37 +01:00
if ( ! $isSingleton && ( ! isset ( $this -> record [ 'ID' ]) || ! $this -> record [ 'ID' ])) {
2007-07-19 12:40:28 +02:00
$this -> populateDefaults ();
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
// prevent populateDefaults() and setField() from marking overwritten defaults as changed
$this -> changed = array ();
2011-05-01 07:33:02 +02:00
$this -> model = $model ? $model : DataModel :: inst ();
}
/**
* Set the DataModel
*/
2012-09-19 12:07:39 +02:00
public function setDataModel ( DataModel $model ) {
2011-05-01 07:33:02 +02:00
$this -> model = $model ;
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
2012-04-12 07:03:18 +02:00
* Destroy all of this objects dependant objects and local caches .
2007-07-19 12:40:28 +02:00
* You ' ll need to call this to get the memory of an object that has components or extensions freed .
*/
2012-09-19 12:07:39 +02:00
public function destroy () {
2012-06-11 12:38:59 +02:00
//$this->destroyed = true;
gc_collect_cycles ();
2010-04-12 07:04:34 +02:00
$this -> flushCache ( false );
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Create a duplicate of this node .
2010-09-16 05:40:34 +02:00
* Note : now also duplicates relations .
2008-08-09 07:04:15 +02:00
*
2007-08-16 08:32:49 +02:00
* @ param $doWrite Perform a write () operation before returning the object . If this is true , it will create the duplicate in the database .
2007-07-19 12:40:28 +02:00
* @ return DataObject A duplicate of this node . The exact type will be the type of this node .
*/
2012-09-19 12:07:39 +02:00
public function duplicate ( $doWrite = true ) {
2007-07-19 12:40:28 +02:00
$className = $this -> class ;
2012-04-20 00:08:17 +02:00
$clone = new $className ( $this -> toMap (), false , $this -> model );
2007-07-19 12:40:28 +02:00
$clone -> ID = 0 ;
2011-09-09 14:31:03 +02:00
$clone -> extend ( 'onBeforeDuplicate' , $this , $doWrite );
2010-09-16 05:40:34 +02:00
if ( $doWrite ) {
$clone -> write ();
$this -> duplicateManyManyRelations ( $this , $clone );
}
2011-09-09 14:31:03 +02:00
$clone -> extend ( 'onAfterDuplicate' , $this , $doWrite );
2007-07-19 12:40:28 +02:00
return $clone ;
}
2007-09-15 01:10:25 +02:00
2010-09-16 05:40:34 +02:00
/**
* Copies the many_many and belongs_many_many relations from one object to another instance of the name of object
* The destinationObject must be written to the database already and have an ID . Writing is performed automatically when adding the new relations .
2010-09-17 04:27:44 +02:00
*
* @ param $sourceObject the source object to duplicate from
* @ param $destinationObject the destination object to populate with the duplicated relations
* @ return DataObject with the new many_many relations copied in
*/
2010-09-16 05:40:34 +02:00
protected function duplicateManyManyRelations ( $sourceObject , $destinationObject ) {
if ( ! $destinationObject || $destinationObject -> ID < 1 ) user_error ( " Can't duplicate relations for an object that has not been written to the database " , E_USER_ERROR );
//duplicate complex relations
// DO NOT copy has_many relations, because copying the relation would result in us changing the has_one relation
// on the other side of this relation to point at the copy and no longer the original (being a has_one, it can
// only point at one thing at a time). So, all relations except has_many can and are copied
2010-09-16 07:53:16 +02:00
if ( $sourceObject -> has_one ()) foreach ( $sourceObject -> has_one () as $name => $type ) {
2010-09-16 05:40:34 +02:00
$this -> duplicateRelations ( $sourceObject , $destinationObject , $name );
}
2010-09-16 07:53:16 +02:00
if ( $sourceObject -> many_many ()) foreach ( $sourceObject -> many_many () as $name => $type ) { //many_many include belongs_many_many
2010-09-16 05:40:34 +02:00
$this -> duplicateRelations ( $sourceObject , $destinationObject , $name );
}
return $destinationObject ;
}
2010-09-17 04:27:44 +02:00
/**
* Helper function to duplicate relations from one object to another
* @ param $sourceObject the source object to duplicate from
* @ param $destinationObject the destination object to populate with the duplicated relations
* @ param $name the name of the relation to duplicate ( e . g . members )
*/
2010-09-16 05:40:34 +02:00
private function duplicateRelations ( $sourceObject , $destinationObject , $name ) {
$relations = $sourceObject -> $name ();
if ( $relations ) {
2009-11-22 06:16:38 +01:00
if ( $relations instanceOf RelationList ) { //many-to-something relation
2010-09-16 05:40:34 +02:00
if ( $relations -> Count () > 0 ) { //with more than one thing it is related to
foreach ( $relations as $relation ) {
$destinationObject -> $name () -> add ( $relation );
}
}
} else { //one-to-one relation
$destinationObject -> $name = $relations ;
}
}
}
2012-08-29 04:30:10 +02:00
2012-09-19 12:07:39 +02:00
public function getObsoleteClassName () {
2012-08-29 04:30:10 +02:00
$className = $this -> getField ( " ClassName " );
if ( ! ClassInfo :: exists ( $className )) return $className ;
}
2012-09-19 12:07:39 +02:00
public function getClassName () {
2012-08-29 04:30:10 +02:00
$className = $this -> getField ( " ClassName " );
if ( ! ClassInfo :: exists ( $className )) return get_class ( $this );
return $className ;
}
2010-09-16 05:40:34 +02:00
2007-07-19 12:40:28 +02:00
/**
2009-04-27 07:55:25 +02:00
* Set the ClassName attribute . { @ link $class } is also updated .
* Warning : This will produce an inconsistent record , as the object
* instance will not automatically switch to the new subclass .
* Please use { @ link newClassInstance ()} for this purpose ,
* or destroy and reinstanciate the record .
2007-09-15 01:10:25 +02:00
*
2009-04-27 07:55:25 +02:00
* @ param string $className The new ClassName attribute ( a subclass of { @ link DataObject })
2007-07-19 12:40:28 +02:00
*/
2012-09-19 12:07:39 +02:00
public function setClassName ( $className ) {
2009-06-09 05:29:24 +02:00
$className = trim ( $className );
if ( ! $className || ! is_subclass_of ( $className , 'DataObject' )) return ;
$this -> class = $className ;
2007-07-19 12:40:28 +02:00
$this -> setField ( " ClassName " , $className );
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
2009-10-14 08:35:45 +02:00
* Create a new instance of a different class from this object ' s record .
2007-07-19 12:40:28 +02:00
* This is useful when dynamically changing the type of an instance . Specifically ,
2007-09-15 01:10:25 +02:00
* it ensures that the instance of the class is a match for the className of the
2009-04-27 07:55:25 +02:00
* record . Don ' t set the { @ link DataObject -> class } or { @ link DataObject -> ClassName }
* property manually before calling this method , as it will confuse change detection .
2009-10-14 08:35:45 +02:00
*
* If the new class is different to the original class , defaults are populated again
* because this will only occur automatically on instantiation of a DataObject if
* there is no record , or the record has no ID . In this case , we do have an ID but
* we still need to repopulate the defaults .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $newClassName The name of the new class
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return DataObject The new instance of the new class , The exact type will be of the class name provided .
*/
2012-09-19 12:07:39 +02:00
public function newClassInstance ( $newClassName ) {
2009-04-27 07:55:25 +02:00
$originalClass = $this -> ClassName ;
$newInstance = new $newClassName ( array_merge (
$this -> record ,
array (
2009-10-14 08:35:45 +02:00
'ClassName' => $originalClass ,
'RecordClassName' => $originalClass ,
2009-04-27 07:55:25 +02:00
)
2011-05-01 07:33:02 +02:00
), false , $this -> model );
2009-10-14 08:35:45 +02:00
if ( $newClassName != $originalClass ) {
2009-04-27 07:55:25 +02:00
$newInstance -> setClassName ( $newClassName );
2009-10-14 08:35:45 +02:00
$newInstance -> populateDefaults ();
2009-04-27 07:55:25 +02:00
$newInstance -> forceChange ();
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
return $newInstance ;
}
/**
* Adds methods from the extensions .
* Called by Object :: __construct () once per class .
2007-09-15 01:10:25 +02:00
*/
2012-09-19 12:07:39 +02:00
public function defineMethods () {
2007-08-16 08:32:49 +02:00
parent :: defineMethods ();
2009-06-17 09:01:28 +02:00
// Define the extra db fields - this is only necessary for extensions added in the
// class definition. Object::add_extension() will call this at definition time for
// those objects, which is a better mechanism. Perhaps extensions defined inside the
// class def can somehow be applied at definiton time also?
2007-08-16 08:32:49 +02:00
if ( $this -> extension_instances ) foreach ( $this -> extension_instances as $i => $instance ) {
2009-09-18 05:02:19 +02:00
if ( ! $instance -> class ) {
$class = get_class ( $instance );
user_error ( " DataObject::defineMethods(): Please ensure { $class } ::__construct() calls parent::__construct() " , E_USER_ERROR );
}
2007-07-19 12:40:28 +02:00
}
2008-08-09 07:04:15 +02:00
2010-10-13 03:35:19 +02:00
if ( $this -> class == 'DataObject' ) return ;
2007-07-19 12:40:28 +02:00
// Set up accessors for joined items
if ( $manyMany = $this -> many_many ()) {
foreach ( $manyMany as $relationship => $class ) {
$this -> addWrapperMethod ( $relationship , 'getManyManyComponents' );
}
}
if ( $hasMany = $this -> has_many ()) {
foreach ( $hasMany as $relationship => $class ) {
$this -> addWrapperMethod ( $relationship , 'getComponents' );
}
}
if ( $hasOne = $this -> has_one ()) {
foreach ( $hasOne as $relationship => $class ) {
$this -> addWrapperMethod ( $relationship , 'getComponent' );
}
}
2009-10-24 01:27:51 +02:00
if ( $belongsTo = $this -> belongs_to ()) foreach ( array_keys ( $belongsTo ) as $relationship ) {
$this -> addWrapperMethod ( $relationship , 'getComponent' );
}
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Returns true if this object " exists " , i . e . , has a sensible value .
2007-10-02 06:57:24 +02:00
* The default behaviour for a DataObject is to return true if
* the object exists in the database , you can override this in subclasses .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return boolean true if this object exists
*/
public function exists () {
2008-08-09 07:04:15 +02:00
return ( $this -> record && $this -> record [ 'ID' ] > 0 );
2007-07-19 12:40:28 +02:00
}
2008-08-09 07:04:15 +02:00
2010-12-06 23:12:24 +01:00
/**
* Returns TRUE if all values ( other than " ID " ) are
* considered empty ( by weak boolean comparison ) .
* Only checks for fields listed in { @ link custom_database_fields ()}
*
* @ todo Use DBField -> hasValue ()
*
* @ return boolean
*/
2007-08-23 07:47:54 +02:00
public function isEmpty (){
$isEmpty = true ;
2010-12-06 23:12:24 +01:00
$customFields = self :: custom_database_fields ( get_class ( $this ));
if ( $map = $this -> toMap ()){
foreach ( $map as $k => $v ){
// only look at custom fields
if ( ! array_key_exists ( $k , $customFields )) continue ;
$dbObj = ( $v instanceof DBField ) ? $v : $this -> dbObject ( $k );
2012-04-11 04:48:06 +02:00
$isEmpty = ( $isEmpty && ! $dbObj -> exists ());
2007-08-23 07:47:54 +02:00
}
}
return $isEmpty ;
}
2008-08-09 07:04:15 +02:00
2007-07-19 12:40:28 +02:00
/**
* Get the user friendly singular name of this DataObject .
* If the name is not defined ( by redefining $singular_name in the subclass ),
* this returns the class name .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return string User friendly singular name of this DataObject
*/
2012-09-19 12:07:39 +02:00
public function singular_name () {
2009-05-22 05:49:15 +02:00
if ( ! $name = $this -> stat ( 'singular_name' )) {
$name = ucwords ( trim ( strtolower ( preg_replace ( '/_?([A-Z])/' , ' $1' , $this -> class ))));
2007-09-15 01:10:25 +02:00
}
2009-05-22 05:49:15 +02:00
2007-09-15 01:10:25 +02:00
return $name ;
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2008-02-25 03:10:37 +01:00
/**
* Get the translated user friendly singular name of this DataObject
* same as singular_name () but runs it through the translating function
*
* Translating string is in the form :
* $this -> class . SINGULARNAME
* Example :
* Page . SINGULARNAME
*
* @ return string User friendly translated singular name of this DataObject
*/
2012-09-19 12:07:39 +02:00
public function i18n_singular_name () {
2009-01-10 12:35:50 +01:00
return _t ( $this -> class . '.SINGULARNAME' , $this -> singular_name ());
2008-02-25 03:10:37 +01:00
}
2007-07-19 12:40:28 +02:00
/**
* Get the user friendly plural name of this DataObject
* If the name is not defined ( by renaming $plural_name in the subclass ),
* this returns a pluralised version of the class name .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return string User friendly plural name of this DataObject
*/
2012-09-19 12:07:39 +02:00
public function plural_name () {
2007-07-19 12:40:28 +02:00
if ( $name = $this -> stat ( 'plural_name' )) {
2007-09-15 01:10:25 +02:00
return $name ;
} else {
2007-07-19 12:40:28 +02:00
$name = $this -> singular_name ();
if ( substr ( $name , - 1 ) == 'e' ) $name = substr ( $name , 0 , - 1 );
else if ( substr ( $name , - 1 ) == 'y' ) $name = substr ( $name , 0 , - 1 ) . 'ie' ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
return ucfirst ( $name . 's' );
}
}
2008-02-25 03:10:37 +01:00
/**
* Get the translated user friendly plural name of this DataObject
* Same as plural_name but runs it through the translation function
* Translation string is in the form :
* $this -> class . PLURALNAME
* Example :
* Page . PLURALNAME
*
* @ return string User friendly translated plural name of this DataObject
*/
2012-09-19 12:07:39 +02:00
public function i18n_plural_name ()
2008-02-25 03:10:37 +01:00
{
2008-08-09 07:04:15 +02:00
$name = $this -> plural_name ();
2008-02-25 03:10:37 +01:00
return _t ( $this -> class . '.PLURALNAME' , $name );
}
2008-08-11 04:25:44 +02:00
/**
* Standard implementation of a title / label for a specific
* record . Tries to find properties 'Title' or 'Name' ,
* and falls back to the 'ID' . Useful to provide
* user - friendly identification of a record , e . g . in errormessages
* or UI - selections .
*
* Overload this method to have a more specialized implementation ,
* e . g . for an Address record this could be :
* < code >
2012-09-19 12:07:39 +02:00
* function getTitle () {
2008-08-11 04:25:44 +02:00
* return " { $this -> StreetNumber } { $this -> StreetName } { $this -> City } " ;
* }
* </ code >
*
* @ return string
*/
public function getTitle () {
2008-11-17 23:59:17 +01:00
if ( $this -> hasDatabaseField ( 'Title' )) return $this -> getField ( 'Title' );
if ( $this -> hasDatabaseField ( 'Name' )) return $this -> getField ( 'Name' );
2008-08-11 04:25:44 +02:00
return " # { $this -> ID } " ;
}
2008-02-25 03:10:37 +01:00
2008-01-10 04:28:13 +01:00
/**
2007-07-19 12:40:28 +02:00
* Returns the associated database record - in this case , the object itself .
* This is included so that you can call $dataOrController -> data () and get a DataObject all the time .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return DataObject Associated database record
*/
public function data () {
return $this ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Convert this object to a map .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return array The data as a map .
*/
public function toMap () {
2012-07-18 06:28:24 +02:00
$this -> loadLazyFields ();
2007-07-19 12:40:28 +02:00
return $this -> record ;
}
/**
2008-10-02 02:45:13 +02:00
* Update a number of fields on this object , given a map of the desired changes .
*
2008-10-02 15:58:08 +02:00
* The field names can be simple names , or you can use a dot syntax to access $has_one relations .
2008-10-02 02:45:13 +02:00
* For example , array ( " Author.FirstName " => " Jim " ) will set $this -> Author () -> FirstName to " Jim " .
*
* update () doesn ' t write the main object , but if you use the dot syntax , it will write ()
* the related objects that it alters .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param array $data A map of field name to data values to update .
*/
public function update ( $data ) {
foreach ( $data as $k => $v ) {
2008-10-02 02:45:13 +02:00
// Implement dot syntax for updates
if ( strpos ( $k , '.' ) !== false ) {
$relations = explode ( '.' , $k );
$fieldName = array_pop ( $relations );
$relObj = $this ;
foreach ( $relations as $i => $relation ) {
2008-10-02 15:58:08 +02:00
// no support for has_many or many_many relationships,
// as the updater wouldn't know which object to write to (or create)
2008-10-03 00:47:01 +02:00
if ( $relObj -> $relation () instanceof DataObject ) {
2008-10-02 15:58:08 +02:00
$relObj = $relObj -> $relation ();
// If the intermediate relationship objects have been created, then write them
if ( $i < sizeof ( $relation ) - 1 && ! $relObj -> ID ) $relObj -> write ();
} else {
user_error (
" DataObject::update(): Can't traverse relationship ' $relation ', " .
2008-10-03 00:47:01 +02:00
" it has to be a has_one relationship or return a single DataObject " ,
2008-10-02 15:58:08 +02:00
E_USER_NOTICE
);
// unset relation object so we don't write properties to the wrong object
unset ( $relObj );
break ;
}
2008-10-02 02:45:13 +02:00
}
2008-10-03 00:47:01 +02:00
2008-10-02 02:45:13 +02:00
if ( $relObj ) {
$relObj -> $fieldName = $v ;
$relObj -> write ();
$relObj -> flushCache ();
} else {
user_error ( " Couldn't follow dot syntax ' $k ' on ' $this->class ' object " , E_USER_WARNING );
}
} else {
$this -> $k = $v ;
}
2007-07-19 12:40:28 +02:00
}
}
2008-10-02 02:45:13 +02:00
2007-07-19 12:40:28 +02:00
/**
* Pass changes as a map , and try to
* get automatic casting for these fields .
* Doesn ' t write to the database . To write the data ,
* use the write () method .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param array $data A map of field name to data values to update .
*/
public function castedUpdate ( $data ) {
foreach ( $data as $k => $v ) {
$this -> setCastedField ( $k , $v );
}
}
2007-09-15 01:10:25 +02:00
2007-10-18 22:04:02 +02:00
/**
* Merges data and relations from another object of same class ,
* without conflict resolution . Allows to specify which
* dataset takes priority in case its not empty .
* has_one - relations are just transferred with priority 'right' .
* has_many and many_many - relations are added regardless of priority .
2008-08-09 07:04:15 +02:00
*
2007-10-18 22:04:02 +02:00
* Caution : has_many / many_many relations are moved rather than duplicated ,
* meaning they are not connected to the merged object any longer .
* Caution : Just saves updated has_many / many_many relations to the database ,
2008-08-09 07:04:15 +02:00
* doesn ' t write the updated object itself ( just writes the object - properties ) .
2007-10-18 22:04:02 +02:00
* Caution : Does not delete the merged object .
* Caution : Does now overwrite Created date on the original object .
2008-08-09 07:04:15 +02:00
*
2007-10-18 22:04:02 +02:00
* @ param $obj DataObject
* @ param $priority String left | right Determines who wins in case of a conflict ( optional )
* @ param $includeRelations Boolean Merge any existing relations ( optional )
2008-08-09 07:04:15 +02:00
* @ param $overwriteWithEmpty Boolean Overwrite existing left values with empty right values .
2007-10-18 22:04:02 +02:00
* Only applicable with $priority = 'right' . ( optional )
* @ return Boolean
*/
public function merge ( $rightObj , $priority = 'right' , $includeRelations = true , $overwriteWithEmpty = false ) {
$leftObj = $this ;
2008-08-09 07:04:15 +02:00
2007-10-18 22:04:02 +02:00
if ( $leftObj -> ClassName != $rightObj -> ClassName ) {
// we can't merge similiar subclasses because they might have additional relations
2008-08-09 07:04:15 +02:00
user_error ( " DataObject->merge(): Invalid object class ' { $rightObj -> ClassName } '
( expected '{$leftObj->ClassName}' ) . " , E_USER_WARNING);
2007-10-18 22:04:02 +02:00
return false ;
}
if ( ! $rightObj -> ID ) {
2008-08-09 07:04:15 +02:00
user_error ( " DataObject->merge(): Please write your merged-in object to the database before merging,
2007-10-18 22:04:02 +02:00
to make sure all relations are transferred properly . ' ) . " , E_USER_WARNING);
return false ;
}
2008-08-09 07:04:15 +02:00
2007-10-18 22:04:02 +02:00
// makes sure we don't merge data like ID or ClassName
2009-01-07 02:25:43 +01:00
$leftData = $leftObj -> inheritedDatabaseFields ();
$rightData = $rightObj -> inheritedDatabaseFields ();
2008-08-09 07:04:15 +02:00
2007-10-18 22:04:02 +02:00
foreach ( $rightData as $key => $rightVal ) {
// don't merge conflicting values if priority is 'left'
if ( $priority == 'left' && $leftObj -> { $key } !== $rightObj -> { $key }) continue ;
2008-08-11 01:17:51 +02:00
2007-10-18 22:04:02 +02:00
// don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
if ( $priority == 'right' && ! $overwriteWithEmpty && empty ( $rightObj -> { $key })) continue ;
// TODO remove redundant merge of has_one fields
$leftObj -> { $key } = $rightObj -> { $key };
}
2008-08-09 07:04:15 +02:00
2007-10-18 22:04:02 +02:00
// merge relations
if ( $includeRelations ) {
if ( $manyMany = $this -> many_many ()) {
foreach ( $manyMany as $relationship => $class ) {
$leftComponents = $leftObj -> getManyManyComponents ( $relationship );
$rightComponents = $rightObj -> getManyManyComponents ( $relationship );
if ( $rightComponents && $rightComponents -> exists ()) $leftComponents -> addMany ( $rightComponents -> column ( 'ID' ));
$leftComponents -> write ();
}
}
if ( $hasMany = $this -> has_many ()) {
foreach ( $hasMany as $relationship => $class ) {
$leftComponents = $leftObj -> getComponents ( $relationship );
$rightComponents = $rightObj -> getComponents ( $relationship );
if ( $rightComponents && $rightComponents -> exists ()) $leftComponents -> addMany ( $rightComponents -> column ( 'ID' ));
$leftComponents -> write ();
}
2008-08-09 07:04:15 +02:00
2007-10-18 22:04:02 +02:00
}
if ( $hasOne = $this -> has_one ()) {
foreach ( $hasOne as $relationship => $class ) {
$leftComponent = $leftObj -> getComponent ( $relationship );
$rightComponent = $rightObj -> getComponent ( $relationship );
if ( $leftComponent -> exists () && $rightComponent -> exists () && $priority == 'right' ) {
$leftObj -> { $relationship . 'ID' } = $rightObj -> { $relationship . 'ID' };
}
}
}
}
2008-08-09 07:04:15 +02:00
2007-10-18 22:04:02 +02:00
return true ;
}
2008-08-09 07:04:15 +02:00
2007-07-19 12:40:28 +02:00
/**
* Forces the record to think that all its data has changed .
2009-04-27 07:55:25 +02:00
* Doesn ' t write to the database . Only sets fields as changed
* if they are not already marked as changed .
2007-07-19 12:40:28 +02:00
*/
public function forceChange () {
2012-07-18 06:28:24 +02:00
// Ensure lazy fields loaded
$this -> loadLazyFields ();
2009-07-03 03:21:48 +02:00
// $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
$fieldNames = array_unique ( array_merge ( array_keys ( $this -> record ), array_keys ( $this -> inheritedDatabaseFields ())));
foreach ( $fieldNames as $fieldName ) {
2009-04-27 07:55:25 +02:00
if ( ! isset ( $this -> changed [ $fieldName ])) $this -> changed [ $fieldName ] = 1 ;
2009-07-03 03:21:48 +02:00
// Populate the null values in record so that they actually get written
2012-07-18 06:28:24 +02:00
if ( ! isset ( $this -> record [ $fieldName ])) $this -> record [ $fieldName ] = null ;
2009-04-27 07:55:25 +02:00
}
2009-05-27 02:09:23 +02:00
// @todo Find better way to allow versioned to write a new version after forceChange
if ( $this -> isChanged ( 'Version' )) unset ( $this -> changed [ 'Version' ]);
2007-07-19 12:40:28 +02:00
}
2008-04-26 08:31:52 +02:00
/**
* Validate the current object .
*
* By default , there is no validation - objects are always valid ! However , you can overload this method in your
2012-04-13 06:18:06 +02:00
* DataObject sub - classes to specify custom validation , or use the hook through DataExtension .
2008-04-26 08:31:52 +02:00
*
* Invalid objects won ' t be able to be written - a warning will be thrown and no write will occur . onBeforeWrite ()
* and onAfterWrite () won ' t get called either .
*
* It is expected that you call validate () in your own application to test that an object is valid before attempting
* a write , and respond appropriately if it isnt ' .
*
* @ return A { @ link ValidationResult } object
*/
protected function validate () {
2012-04-13 06:18:06 +02:00
$result = new ValidationResult ();
$this -> extend ( 'validate' , $result );
return $result ;
2008-04-26 08:31:52 +02:00
}
2007-07-19 12:40:28 +02:00
/**
* Event handler called before writing to the database .
* You can overload this to clean up or otherwise process data before writing it to the
* database . Don ' t forget to call parent :: onBeforeWrite (), though !
2008-04-26 08:31:52 +02:00
*
* This called after { @ link $this -> validate ()}, so you can be sure that your data is valid .
2008-11-07 13:18:35 +01:00
*
2011-04-15 11:35:30 +02:00
* @ uses DataExtension -> onBeforeWrite ()
2007-07-19 12:40:28 +02:00
*/
protected function onBeforeWrite () {
$this -> brokenOnWrite = false ;
2008-11-07 13:18:35 +01:00
$dummy = null ;
$this -> extend ( 'onBeforeWrite' , $dummy );
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2008-04-26 08:31:52 +02:00
/**
* Event handler called after writing to the database .
* You can overload this to act upon changes made to the data after it is written .
* $this -> changed will have a record
* database . Don ' t forget to call parent :: onAfterWrite (), though !
2008-11-07 13:18:35 +01:00
*
2011-04-15 11:35:30 +02:00
* @ uses DataExtension -> onAfterWrite ()
2008-04-26 08:31:52 +02:00
*/
protected function onAfterWrite () {
2008-11-07 13:18:35 +01:00
$dummy = null ;
$this -> extend ( 'onAfterWrite' , $dummy );
2008-04-26 08:31:52 +02:00
}
2007-07-19 12:40:28 +02:00
/**
* Event handler called before deleting from the database .
2007-09-15 01:10:25 +02:00
* You can overload this to clean up or otherwise process data before delete this
2007-07-19 12:40:28 +02:00
* record . Don ' t forget to call parent :: onBeforeDelete (), though !
2008-11-07 13:18:35 +01:00
*
2011-04-15 11:35:30 +02:00
* @ uses DataExtension -> onBeforeDelete ()
2007-07-19 12:40:28 +02:00
*/
protected function onBeforeDelete () {
$this -> brokenOnDelete = false ;
2008-11-07 13:18:35 +01:00
$dummy = null ;
$this -> extend ( 'onBeforeDelete' , $dummy );
}
protected function onAfterDelete () {
$this -> extend ( 'onAfterDelete' );
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Load the default values in from the self :: $defaults array .
* Will traverse the defaults of the current class and all its parent classes .
* Called by the constructor when creating new records .
2008-11-07 13:18:35 +01:00
*
2011-04-15 11:35:30 +02:00
* @ uses DataExtension -> populateDefaults ()
2007-07-19 12:40:28 +02:00
*/
public function populateDefaults () {
$classes = array_reverse ( ClassInfo :: ancestry ( $this ));
2009-03-14 01:16:32 +01:00
2007-07-19 12:40:28 +02:00
foreach ( $classes as $class ) {
2012-04-12 18:26:56 +02:00
$defaults = Config :: inst () -> get ( $class , 'defaults' , Config :: UNINHERITED );
2009-03-17 23:22:46 +01:00
2009-06-17 04:13:02 +02:00
if ( $defaults && ! is_array ( $defaults )) {
user_error ( " Bad ' $this->class ' defaults given: " . var_export ( $defaults , true ),
E_USER_WARNING );
$defaults = null ;
}
2007-07-19 12:40:28 +02:00
if ( $defaults ) foreach ( $defaults as $fieldName => $fieldValue ) {
// SRM 2007-03-06: Stricter check
2008-09-23 02:10:41 +02:00
if ( ! isset ( $this -> $fieldName ) || $this -> $fieldName === null ) {
2007-07-19 12:40:28 +02:00
$this -> $fieldName = $fieldValue ;
}
// Set many-many defaults with an array of ids
if ( is_array ( $fieldValue ) && $this -> many_many ( $fieldName )) {
$manyManyJoin = $this -> $fieldName ();
$manyManyJoin -> setByIdList ( $fieldValue );
}
}
if ( $class == 'DataObject' ) {
break ;
}
}
2008-11-05 00:17:14 +01:00
2008-11-07 13:18:35 +01:00
$this -> extend ( 'populateDefaults' );
2007-07-19 12:40:28 +02:00
}
/**
* Writes all changes to this object to the database .
* - It will insert a record whenever ID isn ' t set , otherwise update .
* - All relevant tables will be updated .
* - $this -> onBeforeWrite () gets called beforehand .
* - Extensions such as Versioned will ammend the database - write to ensure that a version is saved .
2008-11-07 13:18:35 +01:00
*
2011-04-15 11:35:30 +02:00
* @ uses DataExtension -> augmentWrite ()
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param boolean $showDebug Show debugging information
* @ param boolean $forceInsert Run INSERT command rather than UPDATE , even if record already exists
* @ param boolean $forceWrite Write to database even if there are no changes
2008-04-10 03:43:31 +02:00
* @ param boolean $writeComponents Call write () on all associated component instances which were previously
* retrieved through { @ link getComponent ()}, { @ link getComponents ()} or { @ link getManyManyComponents ()}
* ( Default : false )
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return int The ID of the record
2008-10-08 22:50:28 +02:00
* @ throws ValidationException Exception that can be caught and handled by the calling function
2007-07-19 12:40:28 +02:00
*/
2008-04-10 03:43:31 +02:00
public function write ( $showDebug = false , $forceInsert = false , $forceWrite = false , $writeComponents = false ) {
2007-07-19 12:40:28 +02:00
$firstWrite = false ;
$this -> brokenOnWrite = true ;
2008-02-25 03:10:37 +01:00
$isNewRecord = false ;
2012-08-29 04:30:10 +02:00
$writeException = null ;
if ( $this -> ObsoleteClassName ) {
$writeException = new ValidationException (
" Object is of class ' { $this -> ObsoleteClassName } ' which doesn't exist - " .
" you need to change the ClassName before you can write it " ,
E_USER_WARNING
);
}
else if ( self :: get_validation_enabled ()) {
2009-03-04 04:44:11 +01:00
$valid = $this -> validate ();
2012-08-29 04:30:10 +02:00
if ( ! $valid -> valid ()) {
$writeException = new ValidationException (
$valid ,
" Validation error writing a $this->class object: " . $valid -> message () . " . Object not written. " ,
E_USER_WARNING
);
2009-03-04 04:44:11 +01:00
}
2008-04-26 08:31:52 +02:00
}
2009-03-04 04:44:11 +01:00
2012-08-29 04:30:10 +02:00
if ( $writeException ) {
// Used by DODs to clean up after themselves, eg, Versioned
$this -> extend ( 'onAfterSkippedWrite' );
throw $writeException ;
return false ;
}
2007-07-19 12:40:28 +02:00
$this -> onBeforeWrite ();
if ( $this -> brokenOnWrite ) {
user_error ( " $this->class has a broken onBeforeWrite() function. Make sure that you call parent::onBeforeWrite(). " , E_USER_ERROR );
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
// New record = everything has changed
if (( $this -> ID && is_numeric ( $this -> ID )) && ! $forceInsert ) {
$dbCommand = 'update' ;
2008-08-09 04:16:46 +02:00
// Update the changed array with references to changed obj-fields
foreach ( $this -> record as $k => $v ) {
2009-01-05 07:19:48 +01:00
if ( is_object ( $v ) && method_exists ( $v , 'isChanged' ) && $v -> isChanged ()) {
2008-08-09 04:16:46 +02:00
$this -> changed [ $k ] = true ;
}
}
2007-07-19 12:40:28 +02:00
} else {
$dbCommand = 'insert' ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
$this -> changed = array ();
foreach ( $this -> record as $k => $v ) {
$this -> changed [ $k ] = 2 ;
}
2008-10-28 02:23:41 +01:00
2007-07-19 12:40:28 +02:00
$firstWrite = true ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
// No changes made
2008-02-25 03:10:37 +01:00
if ( $this -> changed ) {
foreach ( $this -> getClassAncestry () as $ancestor ) {
2008-11-06 05:51:25 +01:00
if ( self :: has_own_table ( $ancestor ))
2008-08-09 07:04:15 +02:00
$ancestry [] = $ancestor ;
2008-02-25 03:10:37 +01:00
}
2007-07-19 12:40:28 +02:00
2008-02-25 03:10:37 +01:00
// Look for some changes to make
2008-10-28 02:23:41 +01:00
if ( ! $forceInsert ) unset ( $this -> changed [ 'ID' ]);
2008-02-25 02:06:39 +01:00
2008-02-25 03:10:37 +01:00
$hasChanges = false ;
foreach ( $this -> changed as $fieldName => $changed ) {
if ( $changed ) {
$hasChanges = true ;
break ;
}
2007-07-19 12:40:28 +02:00
}
2009-05-27 02:09:23 +02:00
2008-02-25 03:10:37 +01:00
if ( $hasChanges || $forceWrite || ! $this -> record [ 'ID' ]) {
2008-08-09 07:04:15 +02:00
// New records have their insert into the base data table done first, so that they can pass the
2008-02-25 03:10:37 +01:00
// generated primary key on to the rest of the manipulation
2009-03-12 01:34:32 +01:00
$baseTable = $ancestry [ 0 ];
if (( ! isset ( $this -> record [ 'ID' ]) || ! $this -> record [ 'ID' ]) && isset ( $ancestry [ 0 ])) {
2007-09-16 18:40:57 +02:00
2009-03-11 22:48:59 +01:00
DB :: query ( " INSERT INTO \" { $baseTable } \" ( \" Created \" ) VALUES ( " . DB :: getConn () -> now () . " ) " );
2008-02-25 03:10:37 +01:00
$this -> record [ 'ID' ] = DB :: getGeneratedID ( $baseTable );
$this -> changed [ 'ID' ] = 2 ;
2007-09-16 18:40:57 +02:00
2008-02-25 03:10:37 +01:00
$isNewRecord = true ;
}
2007-07-19 12:40:28 +02:00
2008-02-25 03:10:37 +01:00
// Divvy up field saving into a number of database manipulations
2008-08-09 04:00:40 +02:00
$manipulation = array ();
2008-02-25 03:10:37 +01:00
if ( isset ( $ancestry ) && is_array ( $ancestry )) {
foreach ( $ancestry as $idx => $class ) {
$classSingleton = singleton ( $class );
2009-03-17 23:22:46 +01:00
2008-08-09 04:00:40 +02:00
foreach ( $this -> record as $fieldName => $fieldValue ) {
2008-09-30 02:20:30 +02:00
if ( isset ( $this -> changed [ $fieldName ]) && $this -> changed [ $fieldName ] && $fieldType = $classSingleton -> hasOwnTableDatabaseField ( $fieldName )) {
2008-10-01 02:55:25 +02:00
$fieldObj = $this -> dbObject ( $fieldName );
2008-08-09 04:00:40 +02:00
if ( ! isset ( $manipulation [ $class ])) $manipulation [ $class ] = array ();
2008-08-09 05:54:55 +02:00
2008-08-15 05:08:03 +02:00
// if database column doesn't correlate to a DBField instance...
if ( ! $fieldObj ) {
2012-03-27 06:57:42 +02:00
$fieldObj = DBField :: create_field ( 'Varchar' , $this -> record [ $fieldName ], $fieldName );
2008-08-15 05:08:03 +02:00
}
2008-08-09 07:04:15 +02:00
2009-05-22 11:07:08 +02:00
// Both CompositeDBFields and regular fields need to be repopulated
$fieldObj -> setValue ( $this -> record [ $fieldName ], $this -> record );
2009-03-11 22:48:59 +01:00
if ( $class != $baseTable || $fieldName != 'ID' )
$fieldObj -> writeToManipulation ( $manipulation [ $class ]);
2008-02-25 03:10:37 +01:00
}
2007-09-16 18:40:57 +02:00
}
2007-09-15 01:10:25 +02:00
2008-02-25 03:10:37 +01:00
// Add the class name to the base object
if ( $idx == 0 ) {
2010-05-25 05:40:37 +02:00
$manipulation [ $class ][ 'fields' ][ " LastEdited " ] = " ' " . SS_Datetime :: now () -> Rfc2822 () . " ' " ;
2008-02-25 03:10:37 +01:00
if ( $dbCommand == 'insert' ) {
2010-05-25 05:40:37 +02:00
$manipulation [ $class ][ 'fields' ][ " Created " ] = " ' " . SS_Datetime :: now () -> Rfc2822 () . " ' " ;
2008-02-25 03:10:37 +01:00
//echo "<li>$this->class - " .get_class($this);
2012-06-02 15:38:12 +02:00
$manipulation [ $class ][ 'fields' ][ " ClassName " ] = DB :: getConn () -> prepStringForDB ( $this -> class );
2008-02-25 03:10:37 +01:00
}
2008-02-07 04:26:41 +01:00
}
2007-07-19 12:40:28 +02:00
2008-02-25 03:10:37 +01:00
// In cases where there are no fields, this 'stub' will get picked up on
2008-11-06 05:51:25 +01:00
if ( self :: has_own_table ( $class )) {
2008-02-25 03:10:37 +01:00
$manipulation [ $class ][ 'command' ] = $dbCommand ;
$manipulation [ $class ][ 'id' ] = $this -> record [ 'ID' ];
} else {
unset ( $manipulation [ $class ]);
}
2008-02-25 02:06:39 +01:00
}
2008-02-07 04:26:41 +01:00
}
2008-02-25 03:10:37 +01:00
$this -> extend ( 'augmentWrite' , $manipulation );
2008-11-07 13:18:35 +01:00
2008-02-25 03:10:37 +01:00
// New records have their insert into the base data table done first, so that they can pass the
// generated ID on to the rest of the manipulation
if ( isset ( $isNewRecord ) && $isNewRecord && isset ( $manipulation [ $baseTable ])) {
$manipulation [ $baseTable ][ 'command' ] = 'update' ;
}
2009-05-29 01:18:01 +02:00
2008-02-25 03:10:37 +01:00
DB :: manipulate ( $manipulation );
2008-04-26 08:31:52 +02:00
$this -> onAfterWrite ();
2008-02-25 03:10:37 +01:00
$this -> changed = null ;
} elseif ( $showDebug ) {
echo " <b>Debug:</b> no changes for DataObject<br /> " ;
2010-10-12 23:48:07 +02:00
// Used by DODs to clean up after themselves, eg, Versioned
$this -> extend ( 'onAfterSkippedWrite' );
2008-02-07 04:26:41 +01:00
}
2008-02-25 02:06:39 +01:00
2008-02-25 03:10:37 +01:00
// Clears the cache for this object so get_one returns the correct object.
$this -> flushCache ();
2007-07-19 12:40:28 +02:00
2008-02-25 03:10:37 +01:00
if ( ! isset ( $this -> record [ 'Created' ])) {
2010-05-25 05:40:37 +02:00
$this -> record [ 'Created' ] = SS_Datetime :: now () -> Rfc2822 ();
2008-02-25 03:10:37 +01:00
}
2010-05-25 05:40:37 +02:00
$this -> record [ 'LastEdited' ] = SS_Datetime :: now () -> Rfc2822 ();
2010-10-12 23:48:07 +02:00
} else {
// Used by DODs to clean up after themselves, eg, Versioned
$this -> extend ( 'onAfterSkippedWrite' );
2008-02-25 03:10:37 +01:00
}
2008-02-25 02:06:39 +01:00
2009-11-22 06:29:24 +01:00
// Write relations as necessary
2008-04-10 03:43:31 +02:00
if ( $writeComponents ) {
$this -> writeComponents ( true );
2007-07-19 12:40:28 +02:00
}
return $this -> record [ 'ID' ];
}
2007-09-15 01:10:25 +02:00
2008-04-10 03:43:31 +02:00
/**
* Write the cached components to the database . Cached components could refer to two different instances of the same record .
2012-04-12 07:03:18 +02:00
*
2008-04-10 03:43:31 +02:00
* @ param $recursive Recursively write components
*/
public function writeComponents ( $recursive = false ) {
if ( ! $this -> components ) return ;
foreach ( $this -> components as $component ) {
$component -> write ( false , false , false , $recursive );
}
}
2007-07-19 12:40:28 +02:00
/**
* Delete this data object .
* $this -> onBeforeDelete () gets called .
* Note that in Versioned objects , both Stage and Live will be deleted .
2011-04-15 11:35:30 +02:00
* @ uses DataExtension -> augmentSQL ()
2007-07-19 12:40:28 +02:00
*/
2007-09-15 01:10:25 +02:00
public function delete () {
2007-07-19 12:40:28 +02:00
$this -> brokenOnDelete = true ;
$this -> onBeforeDelete ();
if ( $this -> brokenOnDelete ) {
user_error ( " $this->class has a broken onBeforeDelete() function. Make sure that you call parent::onBeforeDelete(). " , E_USER_ERROR );
}
2008-11-07 13:18:35 +01:00
2009-11-22 06:16:38 +01:00
// Deleting a record without an ID shouldn't do anything
if ( ! $this -> ID ) throw new Exception ( " DataObject::delete() called on a DataObject without an ID " );
// TODO: This is quite ugly. To improve:
// - move the details of the delete code in the DataQuery system
// - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
// obviously, that means getting requireTable() to configure cascading deletes ;-)
2011-05-01 07:33:02 +02:00
$srcQuery = DataList :: create ( $this -> class , $this -> model ) -> where ( " ID = $this->ID " ) -> dataQuery () -> query ();
2009-11-22 06:16:38 +01:00
foreach ( $srcQuery -> queriedTables () as $table ) {
2012-05-03 09:34:16 +02:00
$query = new SQLQuery ( " * " , array ( '"' . $table . '"' ));
$query -> setWhere ( " \" ID \" = $this->ID " );
$query -> setDelete ( true );
2009-11-22 06:16:38 +01:00
$query -> execute ();
2007-07-19 12:40:28 +02:00
}
2010-10-04 06:50:43 +02:00
// Remove this item out of any caches
$this -> flushCache ();
2008-11-07 13:18:35 +01:00
$this -> onAfterDelete ();
2007-07-19 12:40:28 +02:00
$this -> OldID = $this -> ID ;
$this -> ID = 0 ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Delete the record with the given ID .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $className The class name of the record to be deleted
* @ param int $id ID of record to be deleted
*/
public static function delete_by_id ( $className , $id ) {
$obj = DataObject :: get_by_id ( $className , $id );
if ( $obj ) {
$obj -> delete ();
} else {
user_error ( " $className object # $id wasn't found when calling DataObject::delete_by_id " , E_USER_WARNING );
}
}
/**
* Get the class ancestry , including the current class name .
* The ancestry will be returned as an array of class names , where the 0 th element
* will be the class that inherits directly from DataObject , and the last element
* will be the current class .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return array Class ancestry
*/
public function getClassAncestry () {
2012-04-12 07:03:18 +02:00
if ( ! isset ( DataObject :: $_cache_get_class_ancestry [ $this -> class ])) {
DataObject :: $_cache_get_class_ancestry [ $this -> class ] = array ( $this -> class );
while (( $class = get_parent_class ( DataObject :: $_cache_get_class_ancestry [ $this -> class ][ 0 ])) != " DataObject " ) {
array_unshift ( DataObject :: $_cache_get_class_ancestry [ $this -> class ], $class );
2007-07-19 12:40:28 +02:00
}
}
2012-04-12 07:03:18 +02:00
return DataObject :: $_cache_get_class_ancestry [ $this -> class ];
2007-07-19 12:40:28 +02:00
}
/**
* Return a component object from a one to one relationship , as a DataObject .
* If no component is available , an 'empty component' will be returned .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $componentName Name of the component
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return DataObject The component object . It ' s exact type will be that of the component .
*/
public function getComponent ( $componentName ) {
if ( isset ( $this -> components [ $componentName ])) {
return $this -> components [ $componentName ];
}
2009-10-24 01:27:51 +02:00
if ( $class = $this -> has_one ( $componentName )) {
$joinField = $componentName . 'ID' ;
$joinID = $this -> getField ( $joinField );
if ( $joinID ) {
2011-05-01 07:33:02 +02:00
$component = $this -> model -> $class -> byID ( $joinID );
2007-07-19 12:40:28 +02:00
}
2009-10-24 01:27:51 +02:00
if ( ! isset ( $component ) || ! $component ) {
2011-05-01 07:33:02 +02:00
$component = $this -> model -> $class -> newObject ();
2007-07-19 12:40:28 +02:00
}
2009-10-24 01:27:51 +02:00
} elseif ( $class = $this -> belongs_to ( $componentName )) {
$joinField = $this -> getRemoteJoinField ( $componentName , 'belongs_to' );
$joinID = $this -> ID ;
if ( $joinID ) {
$component = DataObject :: get_one ( $class , " \" $joinField\ " = $joinID " );
}
if ( ! isset ( $component ) || ! $component ) {
2011-05-01 07:33:02 +02:00
$component = $this -> model -> $class -> newObject ();
2009-10-24 01:27:51 +02:00
$component -> $joinField = $this -> ID ;
2007-07-19 12:40:28 +02:00
}
} else {
2009-10-24 01:27:51 +02:00
throw new Exception ( " DataObject->getComponent(): Could not find component ' $componentName '. " );
2007-07-19 12:40:28 +02:00
}
2009-10-24 01:27:51 +02:00
$this -> components [ $componentName ] = $component ;
return $component ;
2007-07-19 12:40:28 +02:00
}
2008-08-11 01:17:51 +02:00
2007-07-19 12:40:28 +02:00
/**
2009-11-22 06:29:24 +01:00
* Returns a one - to - many relation as a HasManyList
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $componentName Name of the component
* @ param string $filter A filter to be inserted into the WHERE clause
2008-08-09 07:57:44 +02:00
* @ 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 .
2007-07-19 12:40:28 +02:00
* @ param string $join A single join clause . This can be used for filtering , only 1 instance of each DataObject will be returned .
2008-08-09 07:57:44 +02:00
* @ param string | array $limit A limit expression to be inserted into the LIMIT clause
2007-09-15 01:10:25 +02:00
*
2009-11-22 06:29:24 +01:00
* @ return HasManyList The components of the one - to - many relationship .
2007-07-19 12:40:28 +02:00
*/
2012-03-09 03:15:55 +01:00
public function getComponents ( $componentName , $filter = " " , $sort = " " , $join = " " , $limit = null ) {
2008-02-25 03:10:37 +01:00
$result = null ;
2008-08-09 07:04:15 +02:00
2007-07-19 12:40:28 +02:00
if ( ! $componentClass = $this -> has_many ( $componentName )) {
user_error ( " DataObject::getComponents(): Unknown 1-to-many component ' $componentName ' on class ' $this->class ' " , E_USER_ERROR );
}
2008-08-09 07:04:15 +02:00
2009-10-24 01:27:51 +02:00
$joinField = $this -> getRemoteJoinField ( $componentName , 'has_many' );
2009-11-22 06:29:24 +01:00
$result = new HasManyList ( $componentClass , $joinField );
2012-05-01 04:43:52 +02:00
if ( $this -> model ) $result -> setDataModel ( $this -> model );
2012-07-23 00:31:47 +02:00
$result = $result -> forForeignID ( $this -> ID );
2008-08-09 07:04:15 +02:00
2011-10-29 06:11:27 +02:00
$result = $result -> where ( $filter ) -> limit ( $limit ) -> sort ( $sort );
if ( $join ) $result = $result -> join ( $join );
2008-02-25 02:06:39 +01:00
2007-07-19 12:40:28 +02:00
return $result ;
}
2008-08-11 01:17:51 +02:00
2008-08-09 07:57:44 +02:00
/**
* Get the query object for a $has_many Component .
2008-08-11 01:17:51 +02:00
*
2008-08-09 07:57:44 +02:00
* @ param string $componentName
* @ param string $filter
* @ param string | array $sort
* @ param string $join
* @ param string | array $limit
* @ return SQLQuery
*/
public function getComponentsQuery ( $componentName , $filter = " " , $sort = " " , $join = " " , $limit = " " ) {
if ( ! $componentClass = $this -> has_many ( $componentName )) {
user_error ( " DataObject::getComponentsQuery(): Unknown 1-to-many component ' $componentName ' on class ' $this->class ' " , E_USER_ERROR );
}
2009-10-24 01:27:51 +02:00
$joinField = $this -> getRemoteJoinField ( $componentName , 'has_many' );
2008-08-11 01:17:51 +02:00
2008-08-09 07:57:44 +02:00
$id = $this -> getField ( " ID " );
// get filter
2009-09-17 02:09:23 +02:00
$combinedFilter = " \" $joinField\ " = '$id' " ;
2012-09-14 17:05:17 +02:00
if ( ! empty ( $filter )) $combinedFilter .= " AND ( { $filter } ) " ;
2008-08-09 07:57:44 +02:00
return singleton ( $componentClass ) -> extendedSQL ( $combinedFilter , $sort , $limit , $join );
}
2012-04-20 06:03:38 +02:00
/**
* Find the foreign class of a relation on this DataObject , regardless of the relation type .
*
* @ param $relationName Relation name .
* @ return string Class name , or null if not found .
*/
public function getRelationClass ( $relationName ) {
// Go through all relationship configuration fields.
$candidates = array_merge (
( $relations = Config :: inst () -> get ( $this -> class , 'has_one' )) ? $relations : array (),
( $relations = Config :: inst () -> get ( $this -> class , 'has_many' )) ? $relations : array (),
( $relations = Config :: inst () -> get ( $this -> class , 'many_many' )) ? $relations : array (),
( $relations = Config :: inst () -> get ( $this -> class , 'belongs_many_many' )) ? $relations : array (),
( $relations = Config :: inst () -> get ( $this -> class , 'belongs_to' )) ? $relations : array ()
);
if ( isset ( $candidates [ $relationName ])) {
$remoteClass = $candidates [ $relationName ];
// If dot notation is present, extract just the first part that contains the class.
if (( $fieldPos = strpos ( $remoteClass , '.' )) !== false ) {
return substr ( $remoteClass , 0 , $fieldPos );
}
// Otherwise just return the class
return $remoteClass ;
}
return null ;
}
2007-07-19 12:40:28 +02:00
/**
2009-10-24 01:26:10 +02:00
* Tries to find the database key on another object that is used to store a relationship to this class . If no join
* field can be found it defaults to 'ParentID' .
2007-09-15 01:10:25 +02:00
*
2009-10-24 01:26:10 +02:00
* @ param string $component
2009-10-24 01:27:51 +02:00
* @ param string $type the join type - either 'has_many' or 'belongs_to'
2009-10-24 01:26:10 +02:00
* @ return string
2007-07-19 12:40:28 +02:00
*/
2009-10-24 01:27:51 +02:00
public function getRemoteJoinField ( $component , $type = 'has_many' ) {
$remoteClass = $this -> $type ( $component , false );
2009-10-24 01:26:10 +02:00
if ( ! $remoteClass ) {
2009-10-24 01:27:51 +02:00
throw new Exception ( " Unknown $type component ' $component ' on class ' $this->class ' " );
2007-07-19 12:40:28 +02:00
}
2009-10-24 01:26:10 +02:00
if ( $fieldPos = strpos ( $remoteClass , '.' )) {
return substr ( $remoteClass , $fieldPos + 1 ) . 'ID' ;
2007-07-19 12:40:28 +02:00
}
2009-10-24 01:26:10 +02:00
2011-12-22 03:31:56 +01:00
$remoteRelations = array_flip ( Config :: inst () -> get ( $remoteClass , 'has_one' ));
2009-10-24 01:26:10 +02:00
// look for remote has_one joins on this class or any parent classes
foreach ( array_reverse ( ClassInfo :: ancestry ( $this )) as $class ) {
if ( array_key_exists ( $class , $remoteRelations )) return $remoteRelations [ $class ] . 'ID' ;
2007-07-19 12:40:28 +02:00
}
2009-10-24 01:26:10 +02:00
return 'ParentID' ;
2007-07-19 12:40:28 +02:00
}
2009-10-24 01:26:10 +02:00
2008-08-09 07:04:15 +02:00
/**
* Sets the component of a relationship .
2010-10-15 03:12:41 +02:00
* This should only need to be called internally ,
* and is mainly due to the caching logic in { @ link getComponents ()}
* and { @ link getManyManyComponents ()} .
2008-08-09 07:04:15 +02:00
*
* @ param string $componentName Name of the component
2009-11-22 06:29:24 +01:00
* @ param DataObject | HasManyList | ManyManyList $componentValue Value of the component
2012-04-12 07:02:09 +02:00
* @ deprecated 3.0 Not used internally anymore .
2008-08-09 07:04:15 +02:00
*/
public function setComponent ( $componentName , $componentValue ) {
2012-04-12 07:02:09 +02:00
Deprecation :: notice ( '3.0' , 'Not used internally anymore.' );
2008-08-09 07:04:15 +02:00
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
2009-11-22 06:29:24 +01:00
* Returns a many - to - many component , as a ManyManyList .
2007-07-19 12:40:28 +02:00
* @ param string $componentName Name of the many - many component
2009-11-22 06:29:24 +01:00
* @ return ManyManyList The set of components
2007-09-16 18:40:57 +02:00
*
2008-02-25 03:10:37 +01:00
* @ todo Implement query - params
2007-07-19 12:40:28 +02:00
*/
2008-02-25 03:10:37 +01:00
public function getManyManyComponents ( $componentName , $filter = " " , $sort = " " , $join = " " , $limit = " " ) {
2008-08-09 07:57:44 +02:00
list ( $parentClass , $componentClass , $parentField , $componentField , $table ) = $this -> many_many ( $componentName );
2010-10-13 05:58:10 +02:00
2012-06-04 14:01:26 +02:00
$result = Injector :: inst () -> create ( 'ManyManyList' , $componentClass , $table , $componentField , $parentField ,
2009-11-22 06:29:24 +01:00
$this -> many_many_extraFields ( $componentName ));
2012-05-01 04:43:52 +02:00
if ( $this -> model ) $result -> setDataModel ( $this -> model );
2008-08-11 01:17:51 +02:00
2009-11-22 06:29:24 +01:00
// If this is called on a singleton, then we return an 'orphaned relation' that can have the
// foreignID set elsewhere.
2012-07-23 00:31:47 +02:00
$result = $result -> forForeignID ( $this -> ID );
2009-11-22 06:29:24 +01:00
2011-04-05 13:01:57 +02:00
return $result -> where ( $filter ) -> sort ( $sort ) -> limit ( $limit );
2010-04-12 07:04:34 +02:00
}
2007-07-19 12:40:28 +02:00
/**
* Return the class of a one - to - one component . If $component is null , return all of the one - to - one components and their classes .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $component Name of component
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return string | array The class of the one - to - one component , or an array of all one - to - one components and their classes .
*/
public function has_one ( $component = null ) {
$classes = ClassInfo :: ancestry ( $this );
foreach ( $classes as $class ) {
// Wait until after we reach DataObject
2008-04-26 08:53:23 +02:00
if ( in_array ( $class , array ( 'Object' , 'ViewableData' , 'DataObject' ))) continue ;
2007-07-19 12:40:28 +02:00
if ( $component ) {
2012-04-12 18:26:56 +02:00
$hasOne = Config :: inst () -> get ( $class , 'has_one' , Config :: UNINHERITED );
2009-03-14 01:16:32 +01:00
if ( isset ( $hasOne [ $component ])) {
return $hasOne [ $component ];
2007-07-19 12:40:28 +02:00
}
} else {
2012-04-12 18:26:56 +02:00
$newItems = ( array ) Config :: inst () -> get ( $class , 'has_one' , Config :: UNINHERITED );
2008-11-18 02:48:37 +01:00
// Validate the data
foreach ( $newItems as $k => $v ) {
if ( ! is_string ( $k ) || is_numeric ( $k ) || ! is_string ( $v )) user_error ( " $class :: \$ has_one has a bad entry: "
. 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 );
}
$items = isset ( $items ) ? array_merge ( $newItems , ( array ) $items ) : $newItems ;
2007-07-19 12:40:28 +02:00
}
}
return isset ( $items ) ? $items : null ;
}
2009-10-24 01:27:51 +02:00
/**
* Returns the class of a remote belongs_to relationship . If no component is specified a map of all components and
* their class name will be returned .
*
* @ param string $component
* @ param bool $classOnly If this is TRUE , than any has_many relationships in the form " ClassName.Field " will have
* the field data stripped off . It defaults to TRUE .
* @ return string | array
*/
public function belongs_to ( $component = null , $classOnly = true ) {
2011-12-22 03:31:56 +01:00
$belongsTo = $this -> config () -> belongs_to ;
2009-10-24 01:27:51 +02:00
if ( $component ) {
if ( $belongsTo && array_key_exists ( $component , $belongsTo )) {
$belongsTo = $belongsTo [ $component ];
} else {
return false ;
}
}
if ( $belongsTo && $classOnly ) {
return preg_replace ( '/(.+)?\..+/' , '$1' , $belongsTo );
} else {
return $belongsTo ? $belongsTo : array ();
}
}
2007-07-19 12:40:28 +02:00
/**
* Return all of the database fields defined in self :: $db and all the parent classes .
* Doesn ' t include any fields specified by self :: $has_one . Use $this -> has_one () to get these fields
2007-09-15 01:10:25 +02:00
*
2009-06-02 05:45:22 +02:00
* @ param string $fieldName Limit the output to a specific field name
2007-07-19 12:40:28 +02:00
* @ return array The database fields
*/
2009-06-02 05:45:22 +02:00
public function db ( $fieldName = null ) {
2007-07-19 12:40:28 +02:00
$classes = ClassInfo :: ancestry ( $this );
$good = false ;
$items = array ();
2012-04-20 00:08:17 +02:00
2007-07-19 12:40:28 +02:00
foreach ( $classes as $class ) {
// Wait until after we reach DataObject
if ( ! $good ) {
if ( $class == 'DataObject' ) {
$good = true ;
}
continue ;
}
2008-08-09 04:16:46 +02:00
2009-06-02 05:45:22 +02:00
if ( $fieldName ) {
2012-04-12 18:26:56 +02:00
$db = Config :: inst () -> get ( $class , 'db' , Config :: UNINHERITED );
2009-03-14 01:16:32 +01:00
2009-06-02 05:45:22 +02:00
if ( isset ( $db [ $fieldName ])) {
return $db [ $fieldName ];
2008-08-09 04:16:46 +02:00
}
} else {
2012-04-12 18:26:56 +02:00
$newItems = ( array ) Config :: inst () -> get ( $class , 'db' , Config :: UNINHERITED );
2008-11-18 02:48:37 +01:00
// Validate the data
foreach ( $newItems as $k => $v ) {
if ( ! is_string ( $k ) || is_numeric ( $k ) || ! is_string ( $v )) user_error ( " $class :: \$ db has a bad entry: "
. 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 );
}
2009-01-05 07:19:48 +01:00
$items = isset ( $items ) ? array_merge (( array ) $items , $newItems ) : $newItems ;
2008-08-09 04:16:46 +02:00
}
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
return $items ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
2009-10-24 01:26:10 +02:00
* Gets the class of a one - to - many relationship . If no $component is specified then an array of all the one - to - many
* relationships and their classes will be returned .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $component Name of component
2009-10-24 01:26:10 +02:00
* @ param bool $classOnly If this is TRUE , than any has_many relationships in the form " ClassName.Field " will have
* the field data stripped off . It defaults to TRUE .
* @ return string | array
2007-07-19 12:40:28 +02:00
*/
2009-10-24 01:26:10 +02:00
public function has_many ( $component = null , $classOnly = true ) {
2011-12-22 03:31:56 +01:00
$hasMany = $this -> config () -> has_many ;
2009-08-11 10:49:52 +02:00
if ( $component ) {
2009-10-24 01:26:10 +02:00
if ( $hasMany && array_key_exists ( $component , $hasMany )) {
$hasMany = $hasMany [ $component ];
} else {
return false ;
2009-08-11 10:49:52 +02:00
}
2009-10-24 01:26:10 +02:00
}
if ( $hasMany && $classOnly ) {
return preg_replace ( '/(.+)?\..+/' , '$1' , $hasMany );
2009-08-11 10:49:52 +02:00
} else {
2009-10-24 01:26:10 +02:00
return $hasMany ? $hasMany : array ();
2009-08-11 10:49:52 +02:00
}
2007-07-19 12:40:28 +02:00
}
2009-02-10 07:04:36 +01:00
/**
* Return the many - to - many extra fields specification .
*
* If you don ' t specify a component name , it returns all
* extra fields for all components available .
*
* @ param string $component Name of component
* @ return array
*/
public function many_many_extraFields ( $component = null ) {
$classes = ClassInfo :: ancestry ( $this );
foreach ( $classes as $class ) {
if ( in_array ( $class , array ( 'ViewableData' , 'Object' , 'DataObject' ))) continue ;
2010-10-19 00:53:59 +02:00
$relationName = null ;
2009-02-10 07:04:36 +01:00
// Find extra fields for one component
if ( $component ) {
$SNG_class = singleton ( $class );
$extraFields = $SNG_class -> stat ( 'many_many_extraFields' );
// Extra fields are immediately available on this class
if ( isset ( $extraFields [ $component ])) {
return $extraFields [ $component ];
}
$manyMany = $SNG_class -> stat ( 'many_many' );
$candidate = ( isset ( $manyMany [ $component ])) ? $manyMany [ $component ] : null ;
if ( $candidate ) {
$SNG_candidate = singleton ( $candidate );
$candidateManyMany = $SNG_candidate -> stat ( 'belongs_many_many' );
// Find the relation given the class
if ( $candidateManyMany ) foreach ( $candidateManyMany as $relation => $relatedClass ) {
if ( $relatedClass == $class ) {
$relationName = $relation ;
2010-10-13 05:58:10 +02:00
break ;
2009-02-10 07:04:36 +01:00
}
}
2010-10-13 05:58:10 +02:00
if ( $relationName ) {
$extraFields = $SNG_candidate -> stat ( 'many_many_extraFields' );
if ( isset ( $extraFields [ $relationName ])) {
return $extraFields [ $relationName ];
}
2009-02-10 07:04:36 +01:00
}
}
$manyMany = $SNG_class -> stat ( 'belongs_many_many' );
$candidate = ( isset ( $manyMany [ $component ])) ? $manyMany [ $component ] : null ;
if ( $candidate ) {
$SNG_candidate = singleton ( $candidate );
$candidateManyMany = $SNG_candidate -> stat ( 'many_many' );
// Find the relation given the class
if ( $candidateManyMany ) foreach ( $candidateManyMany as $relation => $relatedClass ) {
if ( $relatedClass == $class ) {
$relationName = $relation ;
}
}
$extraFields = $SNG_candidate -> stat ( 'many_many_extraFields' );
if ( isset ( $extraFields [ $relationName ])) {
return $extraFields [ $relationName ];
}
}
} else {
// Find all the extra fields for all components
$newItems = eval ( " return (array) { $class } :: \$ many_many_extraFields; " );
foreach ( $newItems as $k => $v ) {
if ( ! is_array ( $v )) {
user_error (
" $class :: \$ many_many_extraFields has a bad entry: "
. var_export ( $k , true ) . " => " . var_export ( $v , true )
. " . Each many_many_extraFields entry should map to a field specification array. " ,
E_USER_ERROR
);
}
}
return isset ( $items ) ? array_merge ( $newItems , $items ) : $newItems ;
}
}
}
2007-07-19 12:40:28 +02:00
/**
2007-09-15 01:10:25 +02:00
* Return information about a many - to - many component .
2007-07-19 12:40:28 +02:00
* The return value is an array of ( parentclass , childclass ) . If $component is null , then all many - many
* components are returned .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $component Name of component
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return array An array of ( parentclass , childclass ), or an array of all many - many components
*/
public function many_many ( $component = null ) {
$classes = ClassInfo :: ancestry ( $this );
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
foreach ( $classes as $class ) {
// Wait until after we reach DataObject
2008-04-26 08:53:23 +02:00
if ( in_array ( $class , array ( 'ViewableData' , 'Object' , 'DataObject' ))) continue ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
if ( $component ) {
2012-04-12 18:26:56 +02:00
$manyMany = Config :: inst () -> get ( $class , 'many_many' , Config :: UNINHERITED );
2007-07-19 12:40:28 +02:00
// Try many_many
2008-07-18 00:06:34 +02:00
$candidate = ( isset ( $manyMany [ $component ])) ? $manyMany [ $component ] : null ;
2007-07-19 12:40:28 +02:00
if ( $candidate ) {
$parentField = $class . " ID " ;
$childField = ( $class == $candidate ) ? " ChildID " : $candidate . " ID " ;
return array ( $class , $candidate , $parentField , $childField , " { $class } _ $component " );
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
// Try belongs_many_many
2012-04-12 18:26:56 +02:00
$belongsManyMany = Config :: inst () -> get ( $class , 'belongs_many_many' , Config :: UNINHERITED );
2008-07-18 00:06:34 +02:00
$candidate = ( isset ( $belongsManyMany [ $component ])) ? $belongsManyMany [ $component ] : null ;
2007-07-19 12:40:28 +02:00
if ( $candidate ) {
$childField = $candidate . " ID " ;
// We need to find the inverse component name
2012-04-12 18:26:56 +02:00
$otherManyMany = Config :: inst () -> get ( $candidate , 'many_many' , Config :: UNINHERITED );
2007-07-19 12:40:28 +02:00
if ( ! $otherManyMany ) {
2008-08-09 08:40:50 +02:00
user_error ( " Inverse component of $candidate not found ( { $this -> class } ) " , E_USER_ERROR );
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
foreach ( $otherManyMany as $inverseComponentName => $candidateClass ) {
if ( $candidateClass == $class || is_subclass_of ( $class , $candidateClass )) {
$parentField = ( $class == $candidate ) ? " ChildID " : $candidateClass . " ID " ;
// HACK HACK HACK!
if ( $component == 'NestedProducts' ) {
$parentField = $candidateClass . " ID " ;
}
return array ( $class , $candidate , $parentField , $childField , " { $candidate } _ $inverseComponentName " );
}
}
user_error ( " Orphaned \$ belongs_many_many value for $this->class . $component " , E_USER_ERROR );
2007-09-15 01:10:25 +02:00
}
2007-07-19 12:40:28 +02:00
} else {
2012-04-12 18:26:56 +02:00
$newItems = ( array ) Config :: inst () -> get ( $class , 'many_many' , Config :: UNINHERITED );
2008-11-18 02:48:37 +01:00
// Validate the data
foreach ( $newItems as $k => $v ) {
if ( ! is_string ( $k ) || is_numeric ( $k ) || ! is_string ( $v )) user_error ( " $class :: \$ many_many has a bad entry: "
. 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 );
}
$items = isset ( $items ) ? array_merge ( $newItems , $items ) : $newItems ;
2009-03-14 01:16:32 +01:00
2012-04-12 18:26:56 +02:00
$newItems = ( array ) Config :: inst () -> get ( $class , 'belongs_many_many' , Config :: UNINHERITED );
2008-11-18 02:48:37 +01:00
// Validate the data
foreach ( $newItems as $k => $v ) {
if ( ! is_string ( $k ) || is_numeric ( $k ) || ! is_string ( $v )) user_error ( " $class :: \$ belongs_many_many has a bad entry: "
. 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 );
}
$items = isset ( $items ) ? array_merge ( $newItems , $items ) : $newItems ;
2007-07-19 12:40:28 +02:00
}
}
2008-11-18 02:48:37 +01:00
2007-07-19 12:40:28 +02:00
return isset ( $items ) ? $items : null ;
}
2009-10-08 03:21:24 +02:00
/**
* This returns an array ( if it exists ) describing the database extensions that are required , or false if none
*
* This is experimental , and is currently only a Postgres - specific enhancement .
*
* @ return array or false
*/
2012-09-19 12:07:39 +02:00
public function database_extensions ( $class ){
2012-04-12 18:26:56 +02:00
$extensions = Config :: inst () -> get ( $class , 'database_extensions' , Config :: UNINHERITED );
2009-10-08 03:21:24 +02:00
if ( $extensions )
return $extensions ;
else
return false ;
}
2008-08-09 07:04:15 +02:00
2008-08-06 04:43:46 +02:00
/**
* Generates a SearchContext to be used for building and processing
* a generic search form for properties on this object .
2008-08-09 07:04:15 +02:00
*
2008-08-06 04:43:46 +02:00
* @ return SearchContext
*/
public function getDefaultSearchContext () {
2008-08-11 01:29:30 +02:00
return new SearchContext (
$this -> class ,
$this -> scaffoldSearchFields (),
$this -> defaultSearchFilters ()
);
2008-08-06 04:43:46 +02:00
}
2008-08-11 01:29:30 +02:00
2008-08-06 04:43:46 +02:00
/**
* Determine which properties on the DataObject are
* searchable , and map them to their default { @ link FormField }
2008-08-09 06:06:52 +02:00
* representations . Used for scaffolding a searchform for { @ link ModelAdmin } .
2008-08-11 01:17:51 +02:00
*
2008-08-09 07:04:15 +02:00
* Some additional logic is included for switching field labels , based on
* how generic or specific the field type is .
2008-08-06 04:43:46 +02:00
*
2009-03-22 23:59:14 +01:00
* Used by { @ link SearchContext } .
2008-10-05 21:22:54 +02:00
*
2008-10-14 00:20:41 +02:00
* @ param array $_params
* 'fieldClasses' : Associative array of field names as keys and FormField classes as values
* 'restrictFields' : Numeric array of a field name whitelist
2011-10-28 03:37:27 +02:00
* @ return FieldList
2008-08-06 04:43:46 +02:00
*/
2008-10-14 00:20:41 +02:00
public function scaffoldSearchFields ( $_params = null ) {
$params = array_merge (
array (
'fieldClasses' => false ,
'restrictFields' => false
),
( array ) $_params
);
2011-05-11 09:51:54 +02:00
$fields = new FieldList ();
2008-08-11 01:29:30 +02:00
foreach ( $this -> searchableFields () as $fieldName => $spec ) {
2008-10-14 00:20:41 +02:00
if ( $params [ 'restrictFields' ] && ! in_array ( $fieldName , $params [ 'restrictFields' ])) continue ;
2008-08-11 01:29:30 +02:00
2008-10-05 21:22:54 +02:00
// If a custom fieldclass is provided as a string, use it
2008-10-14 00:20:41 +02:00
if ( $params [ 'fieldClasses' ] && isset ( $params [ 'fieldClasses' ][ $fieldName ])) {
$fieldClass = $params [ 'fieldClasses' ][ $fieldName ];
2008-10-05 21:22:54 +02:00
$field = new $fieldClass ( $fieldName );
2008-08-11 01:29:30 +02:00
// If we explicitly set a field, then construct that
2008-10-05 21:22:54 +02:00
} else if ( isset ( $spec [ 'field' ])) {
2008-08-11 05:13:53 +02:00
// If it's a string, use it as a class name and construct
if ( is_string ( $spec [ 'field' ])) {
$fieldClass = $spec [ 'field' ];
$field = new $fieldClass ( $fieldName );
// If it's a FormField object, then just use that object directly.
} else if ( $spec [ 'field' ] instanceof FormField ) {
$field = $spec [ 'field' ];
// Otherwise we have a bug
} else {
user_error ( " Bad value for searchable_fields, 'field' value: " . var_export ( $spec [ 'field' ], true ), E_USER_WARNING );
}
2008-08-11 01:29:30 +02:00
// Otherwise, use the database field's scaffolder
} else {
$field = $this -> relObject ( $fieldName ) -> scaffoldSearchField ();
}
2008-08-09 06:53:34 +02:00
if ( strstr ( $fieldName , '.' )) {
$field -> setName ( str_replace ( '.' , '__' , $fieldName ));
2008-08-06 05:43:48 +02:00
}
2008-08-11 01:29:30 +02:00
$field -> setTitle ( $spec [ 'title' ]);
2008-08-09 06:53:34 +02:00
$fields -> push ( $field );
}
2008-08-06 04:43:46 +02:00
return $fields ;
}
2008-08-09 07:04:15 +02:00
2008-08-06 04:43:46 +02:00
/**
* Scaffold a simple edit form for all properties on this dataobject ,
2008-08-11 01:29:30 +02:00
* based on default { @ link FormField } mapping in { @ link DBField :: scaffoldFormField ()} .
* Field labels / titles will be auto generated from { @ link DataObject :: fieldLabels ()} .
2008-08-06 04:43:46 +02:00
*
2008-10-14 00:20:41 +02:00
* @ uses FormScaffolder
2008-10-05 21:22:54 +02:00
*
2008-10-14 00:20:41 +02:00
* @ param array $_params Associative array passing through properties to { @ link FormScaffolder } .
2011-10-28 03:37:27 +02:00
* @ return FieldList
2008-08-06 04:43:46 +02:00
*/
2008-10-14 00:20:41 +02:00
public function scaffoldFormFields ( $_params = null ) {
$params = array_merge (
array (
'tabbed' => false ,
'includeRelations' => false ,
'restrictFields' => false ,
'fieldClasses' => false ,
'ajaxSafe' => false
),
( array ) $_params
);
2008-10-03 20:38:52 +02:00
2008-10-14 00:20:41 +02:00
$fs = new FormScaffolder ( $this );
$fs -> tabbed = $params [ 'tabbed' ];
$fs -> includeRelations = $params [ 'includeRelations' ];
$fs -> restrictFields = $params [ 'restrictFields' ];
$fs -> fieldClasses = $params [ 'fieldClasses' ];
$fs -> ajaxSafe = $params [ 'ajaxSafe' ];
2008-10-03 20:38:52 +02:00
2012-05-14 05:03:22 +02:00
return $fs -> getFieldList ();
2008-10-03 20:38:52 +02:00
}
2008-10-14 00:20:41 +02:00
2008-08-06 04:43:46 +02:00
/**
* Centerpiece of every data administration interface in Silverstripe ,
2011-10-29 06:09:12 +02:00
* which returns a { @ link FieldList } suitable for a { @ link Form } object .
2008-08-06 04:43:46 +02:00
* If not overloaded , we ' re using { @ link scaffoldFormFields ()} to automatically
2008-10-03 18:21:09 +02:00
* generate this set . To customize , overload this method in a subclass
2011-04-15 11:35:30 +02:00
* or extended onto it by using { @ link DataExtension -> updateCMSFields ()} .
2008-08-09 07:04:15 +02:00
*
2009-03-22 23:59:14 +01:00
* < code >
2008-08-06 05:31:42 +02:00
* klass MyCustomClass extends DataObject {
2008-08-06 04:43:46 +02:00
* static $db = array ( 'CustomProperty' => 'Boolean' );
2008-08-09 07:04:15 +02:00
*
2012-09-19 12:07:39 +02:00
* function getCMSFields () {
2008-08-06 04:43:46 +02:00
* $fields = parent :: getCMSFields ();
2008-10-03 18:21:09 +02:00
* $fields -> addFieldToTab ( 'Root.Content' , new CheckboxField ( 'CustomProperty' ));
2008-08-06 04:43:46 +02:00
* return $fields ;
* }
* }
2009-03-22 23:59:14 +01:00
* </ code >
2008-08-09 07:04:15 +02:00
*
2008-09-26 06:26:06 +02:00
* @ see Good example of complex FormField building : SiteTree :: getCMSFields ()
2008-08-09 07:04:15 +02:00
*
2011-10-28 03:37:27 +02:00
* @ return FieldList Returns a TabSet for usage within the CMS - don ' t use for frontend forms .
2008-08-06 04:43:46 +02:00
*/
2012-04-13 15:46:47 +02:00
public function getCMSFields () {
$tabbedFields = $this -> scaffoldFormFields ( array (
// Don't allow has_many/many_many relationship editing before the record is first saved
'includeRelations' => ( $this -> ID > 0 ),
'tabbed' => true ,
'ajaxSafe' => true
2008-10-14 00:20:41 +02:00
));
2008-10-03 18:21:09 +02:00
2008-10-03 20:38:52 +02:00
$this -> extend ( 'updateCMSFields' , $tabbedFields );
2008-10-03 18:21:09 +02:00
2008-10-03 20:38:52 +02:00
return $tabbedFields ;
2008-10-03 18:21:09 +02:00
}
2008-12-04 23:38:32 +01:00
/**
* need to be overload by solid dataobject , so that the customised actions of that dataobject ,
2011-04-15 11:35:30 +02:00
* including that dataobject ' s extensions customised actions could be added to the EditForm .
2008-12-04 23:38:32 +01:00
*
2011-10-28 03:37:27 +02:00
* @ return an Empty FieldList (); need to be overload by solid subclass
2008-12-04 23:38:32 +01:00
*/
public function getCMSActions () {
2011-05-11 09:51:54 +02:00
$actions = new FieldList ();
2008-12-04 23:38:32 +01:00
$this -> extend ( 'updateCMSActions' , $actions );
return $actions ;
}
2008-10-03 18:21:09 +02:00
/**
* Used for simple frontend forms without relation editing
* or { @ link TabSet } behaviour . Uses { @ link scaffoldFormFields ()}
* by default . To customize , either overload this method in your
2011-04-15 11:35:30 +02:00
* subclass , or extend it by { @ link DataExtension -> updateFrontEndFields ()} .
2008-10-14 00:20:41 +02:00
*
* @ todo Decide on naming for " website|frontend|site|page " and stick with it in the API
2008-10-03 18:21:09 +02:00
*
2008-10-14 00:20:41 +02:00
* @ param array $params See { @ link scaffoldFormFields ()}
2011-10-28 03:37:27 +02:00
* @ return FieldList Always returns a simple field collection without TabSet .
2008-10-03 18:21:09 +02:00
*/
2008-10-14 00:20:41 +02:00
public function getFrontEndFields ( $params = null ) {
$untabbedFields = $this -> scaffoldFormFields ( $params );
2009-02-02 00:49:53 +01:00
$this -> extend ( 'updateFrontEndFields' , $untabbedFields );
2008-10-03 18:21:09 +02:00
2008-10-14 00:20:41 +02:00
return $untabbedFields ;
2008-08-06 04:43:46 +02:00
}
2007-07-19 12:40:28 +02:00
/**
* Gets the value of a field .
* Called by { @ link __get ()} and any getFieldName () methods you might create .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $field The name of the field
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return mixed The field value
*/
2008-11-06 07:32:05 +01:00
public function getField ( $field ) {
2008-08-09 04:16:46 +02:00
// If we already have an object in $this->record, then we should just return that
if ( isset ( $this -> record [ $field ]) && is_object ( $this -> record [ $field ])) return $this -> record [ $field ];
2012-04-20 00:08:17 +02:00
// Do we have a field that needs to be lazy loaded?
if ( isset ( $this -> record [ $field . '_Lazy' ])) {
$tableClass = $this -> record [ $field . '_Lazy' ];
$this -> loadLazyFields ( $tableClass );
}
2008-08-09 04:16:46 +02:00
// Otherwise, we need to determine if this is a complex field
2009-08-11 10:49:52 +02:00
if ( self :: is_composite_field ( $this -> class , $field )) {
2010-10-04 06:46:41 +02:00
$helper = $this -> castingHelper ( $field );
$fieldObj = Object :: create_from_string ( $helper , $field );
2012-07-18 06:25:37 +02:00
$compositeFields = $fieldObj -> compositeDatabaseFields ();
foreach ( $compositeFields as $compositeName => $compositeType ) {
if ( isset ( $this -> record [ $field . $compositeName . '_Lazy' ])) {
$tableClass = $this -> record [ $field . $compositeName . '_Lazy' ];
$this -> loadLazyFields ( $tableClass );
}
}
2009-05-05 09:55:46 +02:00
// write value only if either the field value exists,
// or a valid record has been loaded from the database
$value = ( isset ( $this -> record [ $field ])) ? $this -> record [ $field ] : null ;
2009-05-27 02:09:23 +02:00
if ( $value || $this -> exists ()) $fieldObj -> setValue ( $value , $this -> record , false );
2009-05-05 09:55:46 +02:00
2008-08-09 04:16:46 +02:00
$this -> record [ $field ] = $fieldObj ;
2008-08-11 01:17:51 +02:00
2008-08-09 04:16:46 +02:00
return $this -> record [ $field ];
}
2008-08-09 07:04:15 +02:00
2007-07-19 12:40:28 +02:00
return isset ( $this -> record [ $field ]) ? $this -> record [ $field ] : null ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
2012-04-20 00:08:17 +02:00
* Return a map of all the fields for this record
* @ deprecated 2.4 Use toMap ()
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return array A map of field names to field values .
*/
public function getAllFields () {
2012-05-11 01:23:24 +02:00
Deprecation :: notice ( '3.0' , 'Use toMap() instead.' );
2012-04-20 00:08:17 +02:00
return $this -> toMap ();
}
/**
2012-07-18 06:28:24 +02:00
* Loads all the stub fields that an initial lazy load didn ' t load fully .
2012-04-20 00:08:17 +02:00
*
* @ param tableClass Base table to load the values from . Others are joined as required .
2012-07-18 06:28:24 +02:00
* Not specifying a tableClass will load all lazy fields from all tables .
2012-04-20 00:08:17 +02:00
*/
protected function loadLazyFields ( $tableClass = null ) {
2012-07-18 06:28:24 +02:00
if ( ! $tableClass ) {
$loaded = array ();
foreach ( $this -> record as $key => $value ) {
if ( strlen ( $key ) > 5 && substr ( $key , - 5 ) == '_Lazy' && ! array_key_exists ( $value , $loaded )) {
$this -> loadLazyFields ( $value );
$loaded [ $value ] = $value ;
}
}
return ;
}
2012-04-20 00:08:17 +02:00
$dataQuery = new DataQuery ( $tableClass );
2012-05-15 07:18:04 +02:00
// TableField sets the record ID to "new" on new row data, so don't try doing anything in that case
if ( ! is_numeric ( $this -> record [ 'ID' ])) return false ;
2012-04-20 00:08:17 +02:00
$dataQuery -> where ( " \" $tableClass\ " . \ " ID \" = { $this -> record [ 'ID' ] } " ) -> limit ( 1 );
$columns = array ();
// Add SQL for fields, both simple & multi-value
// TODO: This is copy & pasted from buildSQL(), it could be moved into a method
$databaseFields = self :: database_fields ( $tableClass );
if ( $databaseFields ) foreach ( $databaseFields as $k => $v ) {
if ( ! isset ( $this -> record [ $k ]) || $this -> record [ $k ] === null ) {
$columns [] = $k ;
}
}
if ( $columns ) {
$query = $dataQuery -> query (); // eh?
$this -> extend ( 'augmentSQL' , $query , $dataQuery );
$dataQuery -> setQueriedColumns ( $columns );
$newData = $dataQuery -> execute () -> record ();
// Load the data into record
if ( $newData ) {
foreach ( $newData as $k => $v ) {
if ( in_array ( $k , $columns )) {
$this -> record [ $k ] = $v ;
$this -> original [ $k ] = $v ;
unset ( $this -> record [ $k . '_Lazy' ]);
}
}
// No data means that the query returned nothing; assign 'null' to all the requested fields
} else {
foreach ( $columns as $k ) {
$this -> record [ $k ] = null ;
$this -> original [ $k ] = null ;
unset ( $this -> record [ $k . '_Lazy' ]);
}
}
}
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Return the fields that have changed .
2009-05-27 02:09:23 +02:00
*
2007-07-19 12:40:28 +02:00
* The change level affects what the functions defines as " changed " :
2009-05-27 02:09:23 +02:00
* - Level 1 will return strict changes , even !== ones .
* - Level 2 is more lenient , it will only return real data changes , for example a change from 0 to null
2007-07-19 12:40:28 +02:00
* would not be included .
2007-09-15 01:10:25 +02:00
*
2008-08-11 05:13:53 +02:00
* Example return :
* < code >
* array (
* 'Title' = array ( 'before' => 'Home' , 'after' => 'Home-Changed' , 'level' => 2 )
* )
* </ code >
*
2007-07-19 12:40:28 +02:00
* @ param boolean $databaseFieldsOnly Get only database fields that have changed
* @ param int $changeLevel The strictness of what is defined as change
2008-08-11 05:13:53 +02:00
* @ return array
2007-07-19 12:40:28 +02:00
*/
public function getChangedFields ( $databaseFieldsOnly = false , $changeLevel = 1 ) {
2008-08-11 05:13:53 +02:00
$changedFields = array ();
2009-05-23 07:32:16 +02:00
// Update the changed array with references to changed obj-fields
foreach ( $this -> record as $k => $v ) {
if ( is_object ( $v ) && method_exists ( $v , 'isChanged' ) && $v -> isChanged ()) {
2011-10-07 17:35:25 +02:00
$this -> changed [ $k ] = 2 ;
2009-05-23 07:32:16 +02:00
}
}
2007-07-19 12:40:28 +02:00
if ( $databaseFieldsOnly ) {
2010-10-04 06:50:43 +02:00
$databaseFields = $this -> inheritedDatabaseFields ();
$databaseFields [ 'ID' ] = true ;
$databaseFields [ 'LastEdited' ] = true ;
$databaseFields [ 'Created' ] = true ;
$databaseFields [ 'ClassName' ] = true ;
$fields = array_intersect_key (( array ) $this -> changed , $databaseFields );
2007-07-19 12:40:28 +02:00
} else {
$fields = $this -> changed ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
// Filter the list to those of a certain change level
if ( $changeLevel > 1 ) {
2009-05-27 02:09:23 +02:00
if ( $fields ) foreach ( $fields as $name => $level ) {
2007-07-19 12:40:28 +02:00
if ( $level < $changeLevel ) {
unset ( $fields [ $name ]);
}
}
}
2008-08-11 05:13:53 +02:00
2009-07-17 01:47:50 +02:00
if ( $fields ) foreach ( $fields as $name => $level ) {
2008-08-11 05:13:53 +02:00
$changedFields [ $name ] = array (
2008-10-08 04:00:12 +02:00
'before' => array_key_exists ( $name , $this -> original ) ? $this -> original [ $name ] : null ,
'after' => array_key_exists ( $name , $this -> record ) ? $this -> record [ $name ] : null ,
2008-08-11 05:13:53 +02:00
'level' => $level
);
}
2007-09-15 01:10:25 +02:00
2008-08-11 05:13:53 +02:00
return $changedFields ;
2007-07-19 12:40:28 +02:00
}
2009-05-27 02:09:23 +02:00
/**
* Uses { @ link getChangedFields ()} to determine if fields have been changed
* since loading them from the database .
*
2010-10-13 03:48:48 +02:00
* @ param string $fieldName Name of the database field to check , will check for any if not given
2009-05-27 02:09:23 +02:00
* @ param int $changeLevel See { @ link getChangedFields ()}
* @ return boolean
*/
2012-09-19 12:07:39 +02:00
public function isChanged ( $fieldName = null , $changeLevel = 1 ) {
2009-05-27 02:09:23 +02:00
$changed = $this -> getChangedFields ( false , $changeLevel );
2010-10-13 03:48:48 +02:00
if ( ! isset ( $fieldName )) {
return ! empty ( $changed );
}
else {
return array_key_exists ( $fieldName , $changed );
}
2009-05-27 02:09:23 +02:00
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Set the value of the field
* Called by { @ link __set ()} and any setFieldName () methods you might create .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $fieldName Name of the field
2007-12-02 22:28:02 +01:00
* @ param mixed $val New field value
2007-07-19 12:40:28 +02:00
*/
2012-09-19 12:07:39 +02:00
public function setField ( $fieldName , $val ) {
2010-04-12 03:36:30 +02:00
// Situation 1: Passing an DBField
if ( $val instanceof DBField ) {
$val -> Name = $fieldName ;
2012-04-20 00:08:17 +02:00
// If we've just lazy-loaded the column, then we need to populate the $original array by
// called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
// on a call to getChanged()?
2012-07-18 06:25:37 +02:00
$this -> getField ( $fieldName );
2012-04-20 00:08:17 +02:00
2010-04-12 03:36:30 +02:00
$this -> record [ $fieldName ] = $val ;
// Situation 2: Passing a literal or non-DBField object
} else {
// If this is a proper database field, we shouldn't be getting non-DBField objects
if ( is_object ( $val ) && $this -> db ( $fieldName )) {
2009-07-23 02:31:52 +02:00
user_error ( 'DataObject::setField: passed an object that is not a DBField' , E_USER_WARNING );
}
2010-04-12 03:36:30 +02:00
2008-08-09 04:16:46 +02:00
$defaults = $this -> stat ( 'defaults' );
// if a field is not existing or has strictly changed
2008-10-08 04:25:26 +02:00
if ( ! isset ( $this -> record [ $fieldName ]) || $this -> record [ $fieldName ] !== $val ) {
2008-08-09 04:16:46 +02:00
// TODO Add check for php-level defaults which are not set in the db
// TODO Add check for hidden input-fields (readonly) which are not set in the db
2008-10-08 04:00:12 +02:00
// At the very least, the type has changed
$this -> changed [ $fieldName ] = 1 ;
2009-06-04 08:20:32 +02:00
if (( ! isset ( $this -> record [ $fieldName ]) && $val ) || ( isset ( $this -> record [ $fieldName ]) && $this -> record [ $fieldName ] != $val )) {
2008-10-08 04:00:12 +02:00
// Value has changed as well, not just the type
2008-08-09 04:16:46 +02:00
$this -> changed [ $fieldName ] = 2 ;
}
2012-04-20 00:08:17 +02:00
// If we've just lazy-loaded the column, then we need to populate the $original array by
// called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
// on a call to getChanged()?
2012-07-18 06:25:37 +02:00
$this -> getField ( $fieldName );
2012-04-20 00:08:17 +02:00
// Value is always saved back when strict check succeeds.
2008-08-09 04:16:46 +02:00
$this -> record [ $fieldName ] = $val ;
}
2007-07-19 12:40:28 +02:00
}
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Set the value of the field , using a casting object .
* This is useful when you aren ' t sure that a date is in SQL format , for example .
* setCastedField () can also be used , by forms , to set related data . For example , uploaded images
* can be saved into the Image table .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $fieldName Name of the field
* @ param mixed $value New field value
*/
public function setCastedField ( $fieldName , $val ) {
if ( ! $fieldName ) {
user_error ( " DataObject::setCastedField: Called without a fieldName " , E_USER_ERROR );
}
$castingHelper = $this -> castingHelper ( $fieldName );
if ( $castingHelper ) {
2010-10-04 06:46:41 +02:00
$fieldObj = Object :: create_from_string ( $castingHelper , $fieldName );
2008-08-09 04:16:46 +02:00
$fieldObj -> setValue ( $val );
2007-07-19 12:40:28 +02:00
$fieldObj -> saveInto ( $this );
} else {
$this -> $fieldName = $val ;
}
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Returns true if the given field exists
2008-10-09 16:38:46 +02:00
* in a database column on any of the objects tables ,
* or as a dynamic getter with get < fieldName > () .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $field Name of the field
* @ return boolean True if the given field exists
*/
public function hasField ( $field ) {
2009-06-02 05:43:45 +02:00
return (
2012-04-20 00:08:17 +02:00
array_key_exists ( $field , $this -> record )
2009-10-13 00:28:30 +02:00
|| $this -> db ( $field )
2012-04-20 00:08:17 +02:00
|| ( substr ( $field , - 2 ) == 'ID' ) && $this -> has_one ( substr ( $field , 0 , - 2 ))
2009-06-02 05:43:45 +02:00
|| $this -> hasMethod ( " get { $field } " )
);
2007-07-19 12:40:28 +02:00
}
2008-03-12 10:21:49 +01:00
/**
* Returns true if the given field exists as a database column
*
* @ param string $field Name of the field
*
* @ return boolean
*/
public function hasDatabaseField ( $field ) {
2009-03-31 21:34:37 +02:00
// Add base fields which are not defined in static $db
2009-08-11 10:57:14 +02:00
static $fixedFields = array (
2008-11-18 02:49:29 +01:00
'ID' => 'Int' ,
'ClassName' => 'Enum' ,
API CHANGE: Renamed conflicting classes to have an "SS_" namespace, and renamed existing "SS" namespace to "SS_". The affected classes are: HTTPRequest, HTTPResponse, Query, Database, SSBacktrace, SSCli, SSDatetime, SSDatetimeTest, SSLog, SSLogTest, SSLogEmailWriter, SSLogErrorEmailFormatter, SSLogErrorFileFormatter, SSLogFileWriter and SSZendLog.
MINOR: Replaced usage of renamed classes with the new namespaced name.
From: Andrew Short <andrewjshort@gmail.com>
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@90075 467b73ca-7a2a-4603-9d3b-597d59a354a9
2009-10-26 04:06:31 +01:00
'LastEdited' => 'SS_Datetime' ,
'Created' => 'SS_Datetime' ,
2008-11-18 02:49:29 +01:00
);
2009-03-31 21:34:37 +02:00
if ( isset ( $fixedFields [ $field ])) return true ;
2008-11-18 02:49:29 +01:00
2008-11-17 23:59:17 +01:00
return array_key_exists ( $field , $this -> inheritedDatabaseFields ());
2008-03-12 10:21:49 +01:00
}
2008-09-30 02:20:30 +02:00
/**
* Returns the field type of the given field , if it belongs to this class , and not a parent .
* Note that the field type will not include constructor arguments in round brackets , only the classname .
*
* @ param string $field Name of the field
* @ return string The field type of the given field
2009-11-30 01:45:59 +01:00
*/
public function hasOwnTableDatabaseField ( $field ) {
2008-09-30 02:20:30 +02:00
// Add base fields which are not defined in static $db
if ( $field == " ID " ) return " Int " ;
if ( $field == " ClassName " && get_parent_class ( $this ) == " DataObject " ) return " Enum " ;
API CHANGE: Renamed conflicting classes to have an "SS_" namespace, and renamed existing "SS" namespace to "SS_". The affected classes are: HTTPRequest, HTTPResponse, Query, Database, SSBacktrace, SSCli, SSDatetime, SSDatetimeTest, SSLog, SSLogTest, SSLogEmailWriter, SSLogErrorEmailFormatter, SSLogErrorFileFormatter, SSLogFileWriter and SSZendLog.
MINOR: Replaced usage of renamed classes with the new namespaced name.
From: Andrew Short <andrewjshort@gmail.com>
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@90075 467b73ca-7a2a-4603-9d3b-597d59a354a9
2009-10-26 04:06:31 +01:00
if ( $field == " LastEdited " && get_parent_class ( $this ) == " DataObject " ) return " SS_Datetime " ;
if ( $field == " Created " && get_parent_class ( $this ) == " DataObject " ) return " SS_Datetime " ;
2008-09-30 02:20:30 +02:00
2011-04-15 11:35:30 +02:00
// Add fields from Versioned extension
2010-10-19 00:53:19 +02:00
if ( $field == 'Version' && $this -> hasExtension ( 'Versioned' )) {
return 'Int' ;
}
2008-09-30 02:20:30 +02:00
// get cached fieldmap
2012-04-12 06:14:05 +02:00
$fieldMap = isset ( DataObject :: $cache_has_own_table_field [ $this -> class ]) ? DataObject :: $cache_has_own_table_field [ $this -> class ] : null ;
2008-09-30 02:20:30 +02:00
// if no fieldmap is cached, get all fields
if ( ! $fieldMap ) {
2012-04-12 18:26:56 +02:00
$fieldMap = Config :: inst () -> get ( $this -> class , 'db' , Config :: UNINHERITED );
2009-08-11 10:49:52 +02:00
2008-09-30 02:20:30 +02:00
// all $db fields on this specific class (no parents)
2009-08-11 10:49:52 +02:00
foreach ( self :: composite_fields ( $this -> class , false ) as $fieldname => $fieldtype ) {
$combined_db = singleton ( $fieldtype ) -> compositeDatabaseFields ();
foreach ( $combined_db as $name => $type ){
$fieldMap [ $fieldname . $name ] = $type ;
2009-05-05 09:55:46 +02:00
}
}
2008-09-30 02:20:30 +02:00
// all has_one relations on this specific class,
// add foreign key
2012-04-12 18:26:56 +02:00
$hasOne = Config :: inst () -> get ( $this -> class , 'has_one' , Config :: UNINHERITED );
2008-09-30 02:20:30 +02:00
if ( $hasOne ) foreach ( $hasOne as $fieldName => $fieldSchema ) {
2008-10-14 00:20:41 +02:00
$fieldMap [ $fieldName . 'ID' ] = " ForeignKey " ;
2008-09-30 02:20:30 +02:00
}
2009-05-05 09:55:46 +02:00
2008-09-30 02:20:30 +02:00
// set cached fieldmap
2012-04-12 06:14:05 +02:00
DataObject :: $cache_has_own_table_field [ $this -> class ] = $fieldMap ;
2008-09-30 02:20:30 +02:00
}
2009-05-05 09:55:46 +02:00
2008-09-30 02:20:30 +02:00
// Remove string-based "constructor-arguments" from the DBField definition
2009-05-07 08:00:50 +02:00
if ( isset ( $fieldMap [ $field ])) {
if ( is_string ( $fieldMap [ $field ])) return strtok ( $fieldMap [ $field ], '(' );
else return $fieldMap [ $field ][ 'type' ];
}
2008-09-30 02:20:30 +02:00
}
2009-03-14 01:16:32 +01:00
2008-11-06 05:51:25 +01:00
/**
2009-03-14 01:16:32 +01:00
* Returns true if given class has its own table . Uses the rules for whether the table should exist rather than
* actually looking in the database .
*
* @ param string $dataClass
* @ return bool
2008-11-06 05:51:25 +01:00
*/
2010-10-13 03:06:47 +02:00
public static function has_own_table ( $dataClass ) {
2012-04-18 01:01:03 +02:00
if ( ! is_subclass_of ( $dataClass , 'DataObject' )) return false ;
2009-02-02 00:49:53 +01:00
2012-04-12 06:14:05 +02:00
if ( ! isset ( DataObject :: $cache_has_own_table [ $dataClass ])) {
2008-11-06 05:51:25 +01:00
if ( get_parent_class ( $dataClass ) == 'DataObject' ) {
2012-04-12 06:14:05 +02:00
DataObject :: $cache_has_own_table [ $dataClass ] = true ;
2008-11-06 05:51:25 +01:00
} else {
2012-04-12 18:26:56 +02:00
DataObject :: $cache_has_own_table [ $dataClass ] = Config :: inst () -> get ( $dataClass , 'db' , Config :: UNINHERITED ) || Config :: inst () -> get ( $dataClass , 'has_one' , Config :: UNINHERITED );
2008-11-06 05:51:25 +01:00
}
}
2012-04-12 06:14:05 +02:00
return DataObject :: $cache_has_own_table [ $dataClass ];
2008-11-06 05:51:25 +01:00
}
2007-07-19 12:40:28 +02:00
/**
* Returns true if the member is allowed to do the given action .
2010-10-15 03:12:56 +02:00
* See { @ link extendedCan ()} for a more versatile tri - state permission control .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $perm The permission to be checked , such as 'View' .
* @ param Member $member The member whose permissions need checking . Defaults to the currently logged
* in user .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return boolean True if the the member is allowed to do the given action
*/
2012-09-19 12:07:39 +02:00
public function can ( $perm , $member = null ) {
2007-07-19 12:40:28 +02:00
if ( ! isset ( $member )) {
$member = Member :: currentUser ();
}
2008-11-05 00:31:33 +01:00
if ( Permission :: checkMember ( $member , " ADMIN " )) return true ;
2007-07-19 12:40:28 +02:00
if ( $this -> many_many ( 'Can' . $perm )) {
if ( $this -> ParentID && $this -> SecurityType == 'Inherit' ) {
if ( ! ( $p = $this -> Parent )) {
return false ;
}
return $this -> Parent -> can ( $perm , $member );
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
} else {
$permissionCache = $this -> uninherited ( 'permissionCache' );
2007-09-15 01:10:25 +02:00
$memberID = $member ? $member -> ID : 'none' ;
2007-07-19 12:40:28 +02:00
if ( ! isset ( $permissionCache [ $memberID ][ $perm ])) {
if ( $member -> ID ) {
$groups = $member -> Groups ();
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
$groupList = implode ( ', ' , $groups -> column ( " ID " ));
2007-09-15 01:10:25 +02:00
2011-03-23 00:05:44 +01:00
// TODO Fix relation table hardcoding
2007-07-19 12:40:28 +02:00
$query = new SQLQuery (
2008-11-22 04:51:04 +01:00
" \" Page_Can $perm\ " . PageID " ,
array ( " \" Page_Can $perm\ " " ),
2007-07-19 12:40:28 +02:00
" GroupID IN ( $groupList ) " );
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
$permissionCache [ $memberID ][ $perm ] = $query -> execute () -> column ();
if ( $perm == " View " ) {
2011-03-23 00:05:44 +01:00
// TODO Fix relation table hardcoding
2008-11-24 00:28:16 +01:00
$query = new SQLQuery ( " \" SiteTree \" . \" ID \" " , array (
2008-11-22 04:51:04 +01:00
" \" SiteTree \" " ,
2008-11-24 00:28:16 +01:00
" LEFT JOIN \" Page_CanView \" ON \" Page_CanView \" . \" PageID \" = \" SiteTree \" . \" ID \" "
), " \" Page_CanView \" . \" PageID \" IS NULL " );
2007-07-19 12:40:28 +02:00
2008-08-09 07:04:15 +02:00
$unsecuredPages = $query -> execute () -> column ();
if ( $permissionCache [ $memberID ][ $perm ]) {
$permissionCache [ $memberID ][ $perm ] = array_merge ( $permissionCache [ $memberID ][ $perm ], $unsecuredPages );
} else {
$permissionCache [ $memberID ][ $perm ] = $unsecuredPages ;
}
2007-07-19 12:40:28 +02:00
}
2012-04-18 13:10:57 +02:00
Config :: inst () -> update ( $this -> class , 'permissionCache' , $permissionCache );
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
if ( $permissionCache [ $memberID ][ $perm ]) {
return in_array ( $this -> ID , $permissionCache [ $memberID ][ $perm ]);
}
}
} else {
return parent :: can ( $perm , $member );
}
}
2007-09-15 01:10:25 +02:00
2010-10-15 02:33:48 +02:00
/**
2011-04-15 11:35:30 +02:00
* Process tri - state responses from permission - alterting extensions . The extensions are
2010-10-15 03:12:56 +02:00
* expected to return one of three values :
2010-10-15 02:33:48 +02:00
*
2011-04-15 11:35:30 +02:00
* - false : Disallow this permission , regardless of what other extensions say
* - true : Allow this permission , as long as no other extensions return false
2010-10-15 02:33:48 +02:00
* - NULL : Don ' t affect the outcome
*
* This method itself returns a tri - state value , and is designed to be used like this :
*
2010-10-15 03:12:56 +02:00
* < code >
2010-10-15 02:33:48 +02:00
* $extended = $this -> extendedCan ( 'canDoSomething' , $member );
* if ( $extended !== null ) return $extended ;
* else return $normalValue ;
2010-10-15 03:12:56 +02:00
* </ code >
*
* @ param String $methodName Method on the same object , e . g . { @ link canEdit ()}
* @ param Member | int $member
* @ return boolean | null
2010-10-15 02:33:48 +02:00
*/
public function extendedCan ( $methodName , $member ) {
$results = $this -> extend ( $methodName , $member );
if ( $results && is_array ( $results )) {
// Remove NULLs
$results = array_filter ( $results , array ( $this , 'isNotNull' ));
// If there are any non-NULL responses, then return the lowest one of them.
// If any explicitly deny the permission, then we don't get access
if ( $results ) return min ( $results );
}
return null ;
}
2010-10-15 03:12:56 +02:00
2010-10-15 02:33:48 +02:00
/**
* Helper functon for extendedCan
2010-10-15 03:12:56 +02:00
*
* @ param Mixed $value
* @ return boolean
2010-10-15 02:33:48 +02:00
*/
private function isNotNull ( $value ) {
return ! is_null ( $value );
}
2008-08-09 08:40:50 +02:00
/**
* @ param Member $member
* @ return boolean
*/
public function canView ( $member = null ) {
2008-09-22 18:02:03 +02:00
return Permission :: check ( 'ADMIN' , 'any' , $member );
2008-08-09 08:40:50 +02:00
}
/**
* @ param Member $member
* @ return boolean
*/
public function canEdit ( $member = null ) {
2008-09-22 18:02:03 +02:00
return Permission :: check ( 'ADMIN' , 'any' , $member );
2008-08-09 08:40:50 +02:00
}
/**
* @ param Member $member
* @ return boolean
*/
public function canDelete ( $member = null ) {
2008-09-22 18:02:03 +02:00
return Permission :: check ( 'ADMIN' , 'any' , $member );
2008-08-09 08:40:50 +02:00
}
2008-08-11 01:17:51 +02:00
2008-08-09 08:40:50 +02:00
/**
* @ todo Should canCreate be a static method ?
2008-08-11 01:17:51 +02:00
*
2008-08-09 08:40:50 +02:00
* @ param Member $member
* @ return boolean
*/
public function canCreate ( $member = null ) {
2009-05-18 01:15:31 +02:00
return Permission :: check ( 'ADMIN' , 'any' , $member );
2008-08-09 08:40:50 +02:00
}
2008-08-11 01:17:51 +02:00
2007-07-19 12:40:28 +02:00
/**
* Debugging used by Debug :: show ()
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return string HTML data representing this object
*/
public function debug () {
2009-05-27 05:22:52 +02:00
$val = " <h3>Database record: $this->class </h3> \n <ul> \n " ;
2007-07-19 12:40:28 +02:00
if ( $this -> record ) foreach ( $this -> record as $fieldName => $fieldVal ) {
2009-05-27 05:22:52 +02:00
$val .= " \t <li> $fieldName : " . Debug :: text ( $fieldVal ) . " </li> \n " ;
2007-07-19 12:40:28 +02:00
}
2009-05-27 05:22:52 +02:00
$val .= " </ul> \n " ;
2007-07-19 12:40:28 +02:00
return $val ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Return the DBField object that represents the given field .
2008-10-01 02:55:25 +02:00
* This works similarly to obj () with 2 key differences :
* - it still returns an object even when the field has no value .
* - it only matches fields and not methods
* - it matches foreign keys generated by has_one relationships , eg , " ParentID "
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $fieldName Name of the field
* @ return DBField The field as a DBField object
*/
public function dbObject ( $fieldName ) {
2008-10-02 05:28:01 +02:00
// If we have a CompositeDBField object in $this->record, then return that
if ( isset ( $this -> record [ $fieldName ]) && is_object ( $this -> record [ $fieldName ])) {
return $this -> record [ $fieldName ];
2008-10-01 02:55:25 +02:00
// Special case for ID field
2008-10-02 05:28:01 +02:00
} else if ( $fieldName == 'ID' ) {
2008-08-11 01:17:51 +02:00
return new PrimaryKey ( $fieldName , $this );
2008-10-01 02:55:25 +02:00
// General casting information for items in $db or $casting
2010-10-04 06:46:41 +02:00
} else if ( $helper = $this -> castingHelper ( $fieldName )) {
$obj = Object :: create_from_string ( $helper , $fieldName );
2009-05-27 02:09:23 +02:00
$obj -> setValue ( $this -> $fieldName , $this -> record , false );
2008-08-09 07:04:15 +02:00
return $obj ;
2008-10-01 02:55:25 +02:00
// Special case for has_one relationships
} else if ( preg_match ( '/ID$/' , $fieldName ) && $this -> has_one ( substr ( $fieldName , 0 , - 2 ))) {
2012-04-20 00:08:17 +02:00
$val = $this -> $fieldName ;
2012-03-27 06:57:42 +02:00
return DBField :: create_field ( 'ForeignKey' , $val , $fieldName , $this );
2009-11-02 07:27:00 +01:00
// Special case for ClassName
} else if ( $fieldName == 'ClassName' ) {
$val = get_class ( $this );
2012-03-27 06:57:42 +02:00
return DBField :: create_field ( 'Varchar' , $val , $fieldName , $this );
2008-10-01 02:55:25 +02:00
}
2007-07-19 12:40:28 +02:00
}
2008-08-09 07:04:15 +02:00
2008-08-09 06:53:34 +02:00
/**
2008-08-09 07:04:15 +02:00
* Traverses to a DBField referenced by relationships between data objects .
* The path to the related field is specified with dot separated syntax ( eg : Parent . Child . Child . FieldName )
*
* @ param $fieldPath string
2008-08-09 06:53:34 +02:00
* @ return DBField
*/
public function relObject ( $fieldPath ) {
2012-03-24 02:03:55 +01:00
if ( strpos ( $fieldPath , '.' ) !== false ) {
$parts = explode ( '.' , $fieldPath );
$fieldName = array_pop ( $parts );
// Traverse dot syntax
$component = $this ;
foreach ( $parts as $relation ) {
2012-03-30 04:59:47 +02:00
if ( $component instanceof SS_List ) {
if ( method_exists ( $component , $relation )) $component = $component -> $relation ();
else $component = $component -> relation ( $relation );
} else {
$component = $component -> $relation ();
}
2008-08-09 06:53:34 +02:00
}
2012-03-24 02:03:55 +01:00
$object = $component -> dbObject ( $fieldName );
} else {
$object = $this -> dbObject ( $fieldPath );
2008-08-09 06:53:34 +02:00
}
2008-08-11 01:17:51 +02:00
2008-08-09 07:04:15 +02:00
2009-11-22 06:29:24 +01:00
if ( ! ( $object instanceof DBField ) && ! ( $object instanceof DataList )) {
2008-12-04 23:38:32 +01:00
// Todo: come up with a broader range of exception objects to describe differnet kinds of errors programatically
throw new Exception ( " Unable to traverse to related object field [ $fieldPath ] on [ $this->class ] " );
2008-08-09 07:04:15 +02:00
}
return $object ;
2008-08-09 06:53:34 +02:00
}
2008-08-09 07:04:15 +02:00
2012-03-24 02:03:55 +01:00
/**
* Traverses to a field referenced by relationships between data objects , returning the value
* The path to the related field is specified with dot separated syntax ( eg : Parent . Child . Child . FieldName )
*
* @ param $fieldPath string
* @ return string
*/
2012-07-27 04:44:38 +02:00
public function relField ( $fieldName ) {
$component = $this ;
if ( strpos ( $fieldName , '.' ) !== false ) {
$parts = explode ( '.' , $fieldName );
2012-03-24 02:03:55 +01:00
$fieldName = array_pop ( $parts );
// Traverse dot syntax
foreach ( $parts as $relation ) {
2012-03-30 04:59:47 +02:00
if ( $component instanceof SS_List ) {
if ( method_exists ( $component , $relation )) $component = $component -> $relation ();
else $component = $component -> relation ( $relation );
} else {
$component = $component -> $relation ();
}
2012-03-24 02:03:55 +01:00
}
}
2012-07-27 04:44:38 +02:00
if ( $component -> hasMethod ( $fieldName )) return $component -> $fieldName ();
return $component -> $fieldName ;
2012-03-24 02:03:55 +01:00
}
2008-08-09 06:53:34 +02:00
/**
2008-08-11 01:17:51 +02:00
* Temporary hack to return an association name , based on class , to get around the mangle
2008-08-09 06:53:34 +02:00
* of having to deal with reverse lookup of relationships to determine autogenerated foreign keys .
2008-08-11 01:17:51 +02:00
*
* @ return String
2008-08-09 07:04:15 +02:00
*/
2008-08-11 01:17:51 +02:00
public function getReverseAssociation ( $className ) {
if ( is_array ( $this -> many_many ())) {
$many_many = array_flip ( $this -> many_many ());
if ( array_key_exists ( $className , $many_many )) return $many_many [ $className ];
}
2008-08-09 06:53:34 +02:00
if ( is_array ( $this -> has_many ())) {
$has_many = array_flip ( $this -> has_many ());
if ( array_key_exists ( $className , $has_many )) return $has_many [ $className ];
}
if ( is_array ( $this -> has_one ())) {
$has_one = array_flip ( $this -> has_one ());
if ( array_key_exists ( $className , $has_one )) return $has_one [ $className ];
}
2008-08-11 01:17:51 +02:00
return false ;
2008-08-09 06:53:34 +02:00
}
2008-08-09 07:04:15 +02:00
2007-07-19 12:40:28 +02:00
/**
2012-03-19 03:27:52 +01:00
* @ deprecated 3.0 Use DataList :: create and DataList to do your querying
2007-07-19 12:40:28 +02:00
*/
public function buildSQL ( $filter = " " , $sort = " " , $limit = " " , $join = " " , $restrictClasses = true , $having = " " ) {
2012-03-19 03:27:52 +01:00
Deprecation :: notice ( '3.0' , 'Use DataList::create and DataList to do your querying instead.' );
2009-11-22 06:16:38 +01:00
return $this -> extendedSQL ( $filter , $sort , $limit , $join , $having );
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
2012-03-19 03:27:52 +01:00
* @ deprecated 3.0 Use DataList :: create and DataList to do your querying
2007-07-19 12:40:28 +02:00
*/
2009-11-22 06:16:38 +01:00
public function extendedSQL ( $filter = " " , $sort = " " , $limit = " " , $join = " " ){
2012-03-19 03:27:52 +01:00
Deprecation :: notice ( '3.0' , 'Use DataList::create and DataList to do your querying instead.' );
2009-11-22 06:16:38 +01:00
$dataList = DataObject :: get ( $this -> class , $filter , $sort , $join , $limit );
return $dataList -> dataQuery () -> query ();
2007-07-19 12:40:28 +02:00
}
/**
* Return all objects matching the filter
* sub - classes are automatically selected and included
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $callerClass The class of objects to be returned
* @ param string $filter A filter to be inserted into the WHERE clause .
2008-08-09 07:57:44 +02:00
* @ param string | array $sort A sort expression to be inserted into the ORDER BY clause . If omitted , self :: $default_sort will be used .
2007-07-19 12:40:28 +02:00
* @ param string $join A single join clause . This can be used for filtering , only 1 instance of each DataObject will be returned .
2008-08-09 07:57:44 +02:00
* @ param string | array $limit A limit expression to be inserted into the LIMIT clause .
2007-07-19 12:40:28 +02:00
* @ param string $containerClass The container class to return the results in .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return mixed The objects matching the filter , in the class specified by $containerClass
*/
2012-05-28 11:12:06 +02:00
public static function get ( $callerClass = null , $filter = " " , $sort = " " , $join = " " , $limit = null , $containerClass = 'DataList' ) {
2012-04-20 04:58:24 +02:00
if ( $callerClass == null ) {
$callerClass = get_called_class ();
if ( $callerClass == 'DataObject' ) {
2012-05-28 11:12:06 +02:00
throw new \InvalidArgumentException ( 'Call <classname>::get() instead of DataObject::get()' );
2012-04-20 04:58:24 +02:00
}
2012-05-28 11:12:06 +02:00
if ( $filter || $sort || $join || $limit || ( $containerClass != 'DataList' )) {
throw new \InvalidArgumentException ( 'If calling <classname>::get() then you shouldn\'t pass any other arguments' );
2012-04-20 04:58:24 +02:00
}
$result = DataList :: create ( get_called_class ());
2012-05-01 04:43:52 +02:00
$result -> setDataModel ( DataModel :: inst ());
2012-04-20 04:58:24 +02:00
return $result ;
}
2011-10-28 03:36:20 +02:00
// Todo: Determine if we can deprecate for 3.0.0 and use DI or something instead
2009-11-22 06:16:38 +01:00
// Todo: Make the $containerClass method redundant
2012-05-28 11:12:06 +02:00
if ( $containerClass != 'DataList' ) {
2012-07-13 11:37:35 +02:00
Deprecation :: notice ( '3.0' , 'DataObject::get() - $containerClass argument is deprecated.' , Deprecation :: SCOPE_GLOBAL );
2012-05-28 11:12:06 +02:00
}
2012-03-09 04:18:27 +01:00
$result = DataList :: create ( $callerClass ) -> where ( $filter ) -> sort ( $sort );
2012-05-28 11:12:06 +02:00
2012-03-09 04:18:27 +01:00
if ( $limit && strpos ( $limit , ',' ) !== false ) {
$limitArguments = explode ( ',' , $limit );
$result -> limit ( $limitArguments [ 1 ], $limitArguments [ 0 ]);
} elseif ( $limit ) {
$result -> limit ( $limit );
}
2012-05-28 11:12:06 +02:00
2011-10-29 06:11:27 +02:00
if ( $join ) $result = $result -> join ( $join );
2012-05-28 11:12:06 +02:00
2012-05-01 04:43:52 +02:00
$result -> setDataModel ( DataModel :: inst ());
2009-11-22 06:16:38 +01:00
return $result ;
}
/**
2012-03-19 03:27:52 +01:00
* @ deprecated 3.0 Use DataList :: create and DataList to do your querying
2009-11-22 06:16:38 +01:00
*/
public function Aggregate ( $class = null ) {
2012-05-19 02:55:49 +02:00
Deprecation :: notice ( '3.0' , 'Call aggregate methods on a DataList directly instead. In templates ' .
'an example of the new syntax is <% cached List(Member).max(LastEdited) %> instead (check partial-caching.md documentation ' .
'for more details.)' );
2011-10-28 03:36:20 +02:00
2012-05-19 02:55:49 +02:00
if ( $class ) {
2011-05-01 07:33:02 +02:00
$list = new DataList ( $class );
2012-05-01 04:43:52 +02:00
$list -> setDataModel ( DataModel :: inst ());
2011-05-01 07:33:02 +02:00
} else if ( isset ( $this )) {
$list = new DataList ( get_class ( $this ));
2012-05-01 04:43:52 +02:00
$list -> setDataModel ( $this -> model );
2011-05-01 07:33:02 +02:00
}
2012-05-19 02:55:49 +02:00
else throw new InvalidArgumentException ( " DataObject::aggregate() must be called as an instance method or passed a classname " );
2011-05-01 07:33:02 +02:00
return $list ;
2009-11-22 06:16:38 +01:00
}
/**
2012-03-19 03:27:52 +01:00
* @ deprecated 3.0 Use DataList :: create and DataList to do your querying
2009-11-22 06:16:38 +01:00
*/
public function RelationshipAggregate ( $relationship ) {
2012-05-19 02:55:49 +02:00
Deprecation :: notice ( '3.0' , 'Call aggregate methods on a relationship directly instead.' );
2011-10-28 03:36:20 +02:00
2012-05-19 02:55:49 +02:00
return $this -> $relationship ();
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
2012-03-19 03:27:52 +01:00
* DataList :: create ( " Table " ) -> where ( " filter " ) is the same as singleton ( " Table " ) -> instance_get ( " filter " )
2007-07-19 12:40:28 +02:00
*
2012-03-19 03:27:52 +01:00
* @ deprecated 3.0 Use DataList :: create and DataList to do your querying
2009-11-22 06:16:38 +01:00
*
2007-07-19 12:40:28 +02:00
* @ param string $filter A filter to be inserted into the WHERE clause .
* @ param string $sort A sort expression to be inserted into the ORDER BY clause . If omitted , self :: $default_sort will be used .
* @ param string $join A single join clause . This can be used for filtering , only 1 instance of each DataObject will be returned .
* @ param string $limit A limit expression to be inserted into the LIMIT clause .
* @ param string $containerClass The container class to return the results in .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return mixed The objects matching the filter , in the class specified by $containerClass
*/
2008-02-25 03:10:37 +01:00
public function instance_get ( $filter = " " , $sort = " " , $join = " " , $limit = " " , $containerClass = " DataObjectSet " ) {
2012-03-19 03:27:52 +01:00
Deprecation :: notice ( '3.0' , 'Use DataList::create and DataList to do your querying instead.' );
2009-11-22 06:16:38 +01:00
return self :: get ( $this -> class , $filter , $sort , $join , $limit , $containerClass );
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
API CHANGE: Renamed conflicting classes to have an "SS_" namespace, and renamed existing "SS" namespace to "SS_". The affected classes are: HTTPRequest, HTTPResponse, Query, Database, SSBacktrace, SSCli, SSDatetime, SSDatetimeTest, SSLog, SSLogTest, SSLogEmailWriter, SSLogErrorEmailFormatter, SSLogErrorFileFormatter, SSLogFileWriter and SSZendLog.
MINOR: Replaced usage of renamed classes with the new namespaced name.
From: Andrew Short <andrewjshort@gmail.com>
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@90075 467b73ca-7a2a-4603-9d3b-597d59a354a9
2009-10-26 04:06:31 +01:00
* Take a database { @ link SS_Query } and instanciate an object for each record .
2009-11-22 06:16:38 +01:00
*
2011-10-29 01:02:11 +02:00
* @ deprecated 3.0 Replaced by DataList
2007-09-15 01:10:25 +02:00
*
API CHANGE: Renamed conflicting classes to have an "SS_" namespace, and renamed existing "SS" namespace to "SS_". The affected classes are: HTTPRequest, HTTPResponse, Query, Database, SSBacktrace, SSCli, SSDatetime, SSDatetimeTest, SSLog, SSLogTest, SSLogEmailWriter, SSLogErrorEmailFormatter, SSLogErrorFileFormatter, SSLogFileWriter and SSZendLog.
MINOR: Replaced usage of renamed classes with the new namespaced name.
From: Andrew Short <andrewjshort@gmail.com>
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@90075 467b73ca-7a2a-4603-9d3b-597d59a354a9
2009-10-26 04:06:31 +01:00
* @ param SS_Query | array $records The database records , a { @ link SS_Query } object or an array of maps .
2007-07-19 12:40:28 +02:00
* @ param string $containerClass The class to place all of the objects into .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return mixed The new objects in an object of type $containerClass
*/
2012-09-19 12:07:39 +02:00
public function buildDataObjectSet ( $records , $containerClass = " DataObjectSet " , $query = null , $baseClass = null ) {
2011-10-29 01:02:11 +02:00
Deprecation :: notice ( '3.0' , 'Use DataList instead.' );
2011-10-28 03:36:20 +02:00
2007-07-19 12:40:28 +02:00
foreach ( $records as $record ) {
2008-11-18 02:48:37 +01:00
if ( empty ( $record [ 'RecordClassName' ])) {
2007-07-19 12:40:28 +02:00
$record [ 'RecordClassName' ] = $record [ 'ClassName' ];
}
if ( class_exists ( $record [ 'RecordClassName' ])) {
$results [] = new $record [ 'RecordClassName' ]( $record );
} else {
2009-06-04 08:48:44 +02:00
if ( ! $baseClass ) {
user_error ( " Bad RecordClassName ' { $record [ 'RecordClassName' ] } ' and "
. " \$ baseClass not set " , E_USER_ERROR );
} else if ( ! is_string ( $baseClass ) || ! class_exists ( $baseClass )) {
user_error ( " Bad RecordClassName ' { $record [ 'RecordClassName' ] } ' and bad "
. " \$ baseClass ' $baseClass not set " , E_USER_ERROR );
}
2007-07-19 12:40:28 +02:00
$results [] = new $baseClass ( $record );
2007-09-16 18:40:57 +02:00
}
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
if ( isset ( $results )) {
return new $containerClass ( $results );
}
}
/**
* Return the first item matching the given query .
* All calls to get_one () are cached .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $callerClass The class of objects to be returned
* @ param string $filter A filter to be inserted into the WHERE clause
* @ param boolean $cache Use caching
* @ param string $orderby A sort expression to be inserted into the ORDER BY clause .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return DataObject The first item matching the query
*/
public static function get_one ( $callerClass , $filter = " " , $cache = true , $orderby = " " ) {
2010-04-12 04:40:50 +02:00
$SNG = singleton ( $callerClass );
$cacheKey = " { $filter } - { $orderby } " ;
if ( $extra = $SNG -> extend ( 'cacheKeyComponent' )) {
$cacheKey .= '-' . implode ( " - " , $extra );
}
$cacheKey = md5 ( $cacheKey );
2008-08-11 07:31:13 +02:00
// Flush destroyed items out of the cache
2012-04-12 07:03:18 +02:00
if ( $cache && isset ( DataObject :: $_cache_get_one [ $callerClass ][ $cacheKey ]) && DataObject :: $_cache_get_one [ $callerClass ][ $cacheKey ] instanceof DataObject && DataObject :: $_cache_get_one [ $callerClass ][ $cacheKey ] -> destroyed ) {
DataObject :: $_cache_get_one [ $callerClass ][ $cacheKey
2010-04-12 04:40:50 +02:00
] = false ;
2008-08-11 07:31:13 +02:00
}
2012-04-12 07:03:18 +02:00
if ( ! $cache || ! isset ( DataObject :: $_cache_get_one [ $callerClass ][ $cacheKey ])) {
2011-04-05 13:01:57 +02:00
$dl = DataList :: create ( $callerClass ) -> where ( $filter ) -> sort ( $orderby );
2012-05-01 04:43:52 +02:00
$dl -> setDataModel ( DataModel :: inst ());
2009-11-22 06:16:38 +01:00
$item = $dl -> First ();
2007-07-19 12:40:28 +02:00
if ( $cache ) {
2012-04-12 07:03:18 +02:00
DataObject :: $_cache_get_one [ $callerClass ][ $cacheKey ] = $item ;
if ( ! DataObject :: $_cache_get_one [ $callerClass ][ $cacheKey ]) {
DataObject :: $_cache_get_one [ $callerClass ][ $cacheKey ] = false ;
2007-07-19 12:40:28 +02:00
}
}
}
2012-04-12 07:03:18 +02:00
return $cache ? DataObject :: $_cache_get_one [ $callerClass ][ $cacheKey ] : $item ;
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
2008-08-11 05:03:52 +02:00
* Flush the cached results for all relations ( has_one , has_many , many_many )
2012-04-12 07:03:18 +02:00
* Also clears any cached aggregate data .
2012-04-12 02:47:17 +02:00
*
* @ param boolean $persistent When true will also clear persistent data stored in the Cache system .
2010-04-12 07:04:34 +02:00
* When false will just clear session - local cached data
2012-04-12 02:47:17 +02:00
*
2007-07-19 12:40:28 +02:00
*/
2012-04-12 02:47:17 +02:00
public function flushCache ( $persistent = true ) {
if ( $persistent ) Aggregate :: flushCache ( $this -> class );
2010-04-12 07:04:34 +02:00
2007-08-16 08:32:49 +02:00
if ( $this -> class == 'DataObject' ) {
2012-04-12 07:03:18 +02:00
DataObject :: $_cache_get_one = array ();
2008-08-09 07:04:15 +02:00
return ;
2007-08-16 08:32:49 +02:00
}
2007-09-16 18:40:57 +02:00
2007-07-19 12:40:28 +02:00
$classes = ClassInfo :: ancestry ( $this -> class );
foreach ( $classes as $class ) {
2012-04-12 06:14:05 +02:00
if ( isset ( DataObject :: $_cache_get_one [ $class ])) unset ( DataObject :: $_cache_get_one [ $class ]);
2007-07-19 12:40:28 +02:00
}
2008-08-11 05:03:52 +02:00
2008-12-17 23:38:47 +01:00
$this -> extend ( 'flushCache' );
2012-04-12 05:31:44 +02:00
$this -> components = array ();
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2012-04-12 05:31:44 +02:00
/**
* Flush the get_one global cache and destroy associated objects .
*/
2012-09-19 12:07:39 +02:00
public static function flush_and_destroy_cache () {
2012-04-12 06:14:05 +02:00
if ( DataObject :: $_cache_get_one ) foreach ( DataObject :: $_cache_get_one as $class => $items ) {
2008-12-15 02:30:41 +01:00
if ( is_array ( $items )) foreach ( $items as $item ) {
if ( $item ) $item -> destroy ();
}
}
2012-04-12 06:14:05 +02:00
DataObject :: $_cache_get_one = array ();
2008-12-15 02:30:41 +01:00
}
2009-08-11 06:45:54 +02:00
/**
2012-04-12 05:31:44 +02:00
* Reset all global caches associated with DataObject .
2009-08-11 06:45:54 +02:00
*/
2012-09-19 12:07:39 +02:00
public static function reset () {
2012-04-12 06:14:05 +02:00
DataObject :: $cache_has_own_table = array ();
DataObject :: $cache_has_own_table_field = array ();
DataObject :: $_cache_get_one = array ();
DataObject :: $_cache_composite_fields = array ();
DataObject :: $_cache_get_class_ancestry = array ();
2012-04-13 18:38:23 +02:00
DataObject :: $_cache_field_labels = array ();
2009-08-11 06:45:54 +02:00
}
2008-12-15 02:30:41 +01:00
2007-07-19 12:40:28 +02:00
/**
* Does the hard work for get_one ()
2009-11-22 06:16:38 +01:00
*
2012-03-19 03:27:52 +01:00
* @ deprecated 3.0 Use DataList :: create ( $this -> class ) -> where ( $filter ) -> sort ( $orderby ) -> First () instead
2008-11-07 13:18:35 +01:00
*
2011-04-15 11:35:30 +02:00
* @ uses DataExtension -> augmentSQL ()
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $filter A filter to be inserted into the WHERE clause
* @ param string $orderby A sort expression to be inserted into the ORDER BY clause .
* @ return DataObject The first item matching the query
*/
public function instance_get_one ( $filter , $orderby = null ) {
2012-03-19 03:27:52 +01:00
Deprecation :: notice ( '3.0' , 'Use DataList::create($this->class)->where($filter)->sort($orderby)->First() instead.' );
2009-11-22 06:16:38 +01:00
return DataObject :: get_one ( $this -> class , $filter , true , $orderby );
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Return the given element , searching by ID
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $callerClass The class of the object to be returned
* @ param int $id The id of the element
2009-05-27 02:33:02 +02:00
* @ param boolean $cache See { @ link get_one ()}
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return DataObject The element
*/
2009-05-27 02:33:02 +02:00
public static function get_by_id ( $callerClass , $id , $cache = true ) {
2007-07-19 12:40:28 +02:00
if ( is_numeric ( $id )) {
2009-03-14 01:16:32 +01:00
if ( is_subclass_of ( $callerClass , 'DataObject' )) {
2010-12-02 03:51:35 +01:00
$baseClass = ClassInfo :: baseDataClass ( $callerClass );
2011-10-29 06:27:21 +02:00
return DataObject :: get_one ( $callerClass , " \" $baseClass\ " . \ " ID \" = $id " , $cache );
2008-08-09 07:04:15 +02:00
// This simpler code will be used by non-DataObject classes that implement DataObjectInterface
2008-02-25 03:10:37 +01:00
} else {
2011-10-29 06:27:21 +02:00
return DataObject :: get_one ( $callerClass , " \" ID \" = $id " , $cache );
2008-02-25 03:10:37 +01:00
}
2007-07-19 12:40:28 +02:00
} else {
user_error ( " DataObject::get_by_id passed a non-numeric ID # $id " , E_USER_WARNING );
}
}
2008-08-09 07:04:15 +02:00
2008-08-09 06:53:34 +02:00
/**
* Get the name of the base table for this object
*/
public function baseTable () {
$tableClasses = ClassInfo :: dataClassesFor ( $this -> class );
return array_shift ( $tableClasses );
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
//-------------------------------------------------------------------------------------------//
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Return the database indexes on this table .
* This array is indexed by the name of the field with the index , and
* the value is the type of index .
*/
2008-08-09 07:04:15 +02:00
public function databaseIndexes () {
2007-07-19 12:40:28 +02:00
$has_one = $this -> uninherited ( 'has_one' , true );
$classIndexes = $this -> uninherited ( 'indexes' , true );
//$fileIndexes = $this->uninherited('fileIndexes', true);
$indexes = array ();
if ( $has_one ) {
foreach ( $has_one as $relationshipName => $fieldType ) {
$indexes [ $relationshipName . 'ID' ] = true ;
}
}
2007-09-15 01:10:25 +02:00
if ( $classIndexes ) {
2007-07-19 12:40:28 +02:00
foreach ( $classIndexes as $indexName => $indexType ) {
$indexes [ $indexName ] = $indexType ;
}
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
if ( get_parent_class ( $this ) == " DataObject " ) {
$indexes [ 'ClassName' ] = true ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
return $indexes ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Check the database schema and update it as necessary .
2008-11-07 13:18:35 +01:00
*
2011-04-15 11:35:30 +02:00
* @ uses DataExtension -> augmentDatabase ()
2007-07-19 12:40:28 +02:00
*/
public function requireTable () {
2007-09-15 01:10:25 +02:00
// Only build the table if we've actually got fields
2009-08-11 10:49:52 +02:00
$fields = self :: database_fields ( $this -> class );
2009-10-08 03:21:24 +02:00
$extensions = self :: database_extensions ( $this -> class );
2011-12-22 03:31:56 +01:00
2008-08-09 07:04:15 +02:00
$indexes = $this -> databaseIndexes ();
2007-07-19 12:40:28 +02:00
if ( $fields ) {
2009-03-11 22:48:59 +01:00
$hasAutoIncPK = ( $this -> class == ClassInfo :: baseDataClass ( $this -> class ));
2009-10-08 03:21:24 +02:00
DB :: requireTable ( $this -> class , $fields , $indexes , $hasAutoIncPK , $this -> stat ( 'create_table_options' ), $extensions );
2007-07-19 12:40:28 +02:00
} else {
DB :: dontRequireTable ( $this -> class );
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
// Build any child tables for many_many items
if ( $manyMany = $this -> uninherited ( 'many_many' , true )) {
$extras = $this -> uninherited ( 'many_many_extraFields' , true );
foreach ( $manyMany as $relationship => $childClass ) {
// Build field list
$manymanyFields = array (
" { $this -> class } ID " => " Int " ,
2008-08-09 07:04:15 +02:00
(( $this -> class == $childClass ) ? " ChildID " : " { $childClass } ID " ) => " Int " ,
2007-07-19 12:40:28 +02:00
);
2008-10-16 00:57:33 +02:00
if ( isset ( $extras [ $relationship ])) {
2007-07-19 12:40:28 +02:00
$manymanyFields = array_merge ( $manymanyFields , $extras [ $relationship ]);
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
// Build index list
$manymanyIndexes = array (
2012-06-05 01:07:19 +02:00
" { $this -> class } ID " => true ,
(( $this -> class == $childClass ) ? " ChildID " : " { $childClass } ID " ) => true ,
2007-07-19 12:40:28 +02:00
);
2009-10-08 03:21:24 +02:00
DB :: requireTable ( " { $this -> class } _ $relationship " , $manymanyFields , $manymanyIndexes , true , null , $extensions );
2007-07-19 12:40:28 +02:00
}
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
// Let any extentions make their own database fields
$this -> extend ( 'augmentDatabase' , $dummy );
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Add default records to database . This function is called whenever the
* database is built , after the database tables have all been created . Overload
* this to add default records when the database is built , but make sure you
* call parent :: requireDefaultRecords () .
2008-11-07 13:18:35 +01:00
*
2011-04-15 11:35:30 +02:00
* @ uses DataExtension -> requireDefaultRecords ()
2007-07-19 12:40:28 +02:00
*/
public function requireDefaultRecords () {
$defaultRecords = $this -> stat ( 'default_records' );
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
if ( ! empty ( $defaultRecords )) {
2008-08-11 02:14:48 +02:00
$hasData = DataObject :: get_one ( $this -> class );
if ( ! $hasData ) {
$className = $this -> class ;
foreach ( $defaultRecords as $record ) {
2011-05-01 07:33:02 +02:00
$obj = $this -> model -> $className -> newObject ( $record );
2008-08-11 02:14:48 +02:00
$obj -> write ();
2007-07-19 12:40:28 +02:00
}
2009-10-26 23:03:29 +01:00
DB :: alteration_message ( " Added default records to $className table " , " created " );
2007-07-19 12:40:28 +02:00
}
}
2008-11-07 13:18:35 +01:00
// Let any extentions make their own database default data
$this -> extend ( 'requireDefaultRecords' , $dummy );
2007-07-19 12:40:28 +02:00
}
2009-03-14 01:16:32 +01:00
2007-07-19 12:40:28 +02:00
/**
2011-10-29 01:02:11 +02:00
* @ deprecated 3.0 Use DataObject :: database_fields () instead
2009-03-14 01:16:32 +01:00
* @ see DataObject :: database_fields ()
2007-07-19 12:40:28 +02:00
*/
public function databaseFields () {
2011-10-29 01:02:11 +02:00
Deprecation :: notice ( '3.0' , 'Use DataObject::database_fields() instead.' );
2009-03-14 01:16:32 +01:00
return self :: database_fields ( $this -> class );
2007-07-19 12:40:28 +02:00
}
2009-03-14 01:16:32 +01:00
2007-07-19 12:40:28 +02:00
/**
2011-10-29 01:02:11 +02:00
* @ deprecated 3.0 Use DataObject :: custom_database_fields () instead
2009-03-14 01:16:32 +01:00
* @ see DataObject :: custom_database_fields ()
2007-07-19 12:40:28 +02:00
*/
public function customDatabaseFields () {
2011-10-29 01:02:11 +02:00
Deprecation :: notice ( '3.0' , 'Use DataObject::custom_database_fields() instead.' );
2009-03-14 01:16:32 +01:00
return self :: custom_database_fields ( $this -> class );
2007-07-19 12:40:28 +02:00
}
2009-03-14 01:16:32 +01:00
2008-08-06 05:43:48 +02:00
/**
* Returns fields bu traversing the class heirachy in a bottom - up direction .
2008-08-09 07:04:15 +02:00
*
2008-08-06 05:43:48 +02:00
* Needed to avoid getCMSFields being empty when customDatabaseFields overlooks
* the inheritance chain of the $db array , where a child data object has no $db array ,
* but still needs to know the properties of its parent . This should be merged into databaseFields or
* customDatabaseFields .
2008-08-09 07:04:15 +02:00
*
2008-08-09 06:06:52 +02:00
* @ todo review whether this is still needed after recent API changes
2008-08-06 05:43:48 +02:00
*/
public function inheritedDatabaseFields () {
2009-03-14 01:16:32 +01:00
$fields = array ();
$currentObj = $this -> class ;
while ( $currentObj != 'DataObject' ) {
$fields = array_merge ( $fields , self :: custom_database_fields ( $currentObj ));
$currentObj = get_parent_class ( $currentObj );
2008-08-06 05:43:48 +02:00
}
2009-03-14 01:16:32 +01:00
return ( array ) $fields ;
2008-08-06 05:43:48 +02:00
}
2008-08-09 07:04:15 +02:00
2008-08-06 05:43:48 +02:00
/**
* Get the default searchable fields for this object ,
2008-08-09 06:06:52 +02:00
* as defined in the $searchable_fields list . If searchable
* fields are not defined on the data object , uses a default
* selection of summary fields .
2008-08-09 07:04:15 +02:00
*
2008-08-09 06:06:52 +02:00
* @ return array
2008-08-06 05:43:48 +02:00
*/
2008-08-11 01:29:30 +02:00
public function searchableFields () {
// can have mixed format, need to make consistent in most verbose form
2008-08-09 06:06:52 +02:00
$fields = $this -> stat ( 'searchable_fields' );
2008-09-30 02:20:30 +02:00
2008-08-11 01:29:30 +02:00
$labels = $this -> fieldLabels ();
// fallback to summary fields
if ( ! $fields ) $fields = array_keys ( $this -> summaryFields ());
2008-09-30 02:20:30 +02:00
// we need to make sure the format is unified before
2011-04-15 11:35:30 +02:00
// augmenting fields, so extensions can apply consistent checks
// but also after augmenting fields, because the extension
2008-09-30 02:20:30 +02:00
// might use the shorthand notation as well
2008-08-11 01:29:30 +02:00
// rewrite array, if it is using shorthand syntax
$rewrite = array ();
foreach ( $fields as $name => $specOrName ) {
$identifer = ( is_int ( $name )) ? $specOrName : $name ;
2008-09-30 02:20:30 +02:00
2008-08-11 01:29:30 +02:00
if ( is_int ( $name )) {
// Format: array('MyFieldName')
$rewrite [ $identifer ] = array ();
} elseif ( is_array ( $specOrName )) {
// Format: array('MyFieldName' => array(
// 'filter => 'ExactMatchFilter',
// 'field' => 'NumericField', // optional
// 'title' => 'My Title', // optiona.
2008-09-30 02:20:30 +02:00
// ))
2008-08-11 01:29:30 +02:00
$rewrite [ $identifer ] = array_merge (
array ( 'filter' => $this -> relObject ( $identifer ) -> stat ( 'default_search_filter_class' )),
( array ) $specOrName
);
} else {
// Format: array('MyFieldName' => 'ExactMatchFilter')
$rewrite [ $identifer ] = array (
'filter' => $specOrName ,
);
}
if ( ! isset ( $rewrite [ $identifer ][ 'title' ])) {
$rewrite [ $identifer ][ 'title' ] = ( isset ( $labels [ $identifer ])) ? $labels [ $identifer ] : FormField :: name_to_label ( $identifer );
}
if ( ! isset ( $rewrite [ $identifer ][ 'filter' ])) {
$rewrite [ $identifer ][ 'filter' ] = 'PartialMatchFilter' ;
2008-08-09 08:53:26 +02:00
}
2008-08-09 06:06:52 +02:00
}
2008-09-30 02:20:30 +02:00
2008-08-11 01:29:30 +02:00
$fields = $rewrite ;
2008-08-14 01:57:53 +02:00
2011-04-15 11:35:30 +02:00
// apply DataExtensions if present
2008-08-14 01:57:53 +02:00
$this -> extend ( 'updateSearchableFields' , $fields );
2008-08-11 01:29:30 +02:00
2008-08-09 06:06:52 +02:00
return $fields ;
}
2008-08-11 01:29:30 +02:00
2008-08-09 08:53:26 +02:00
/**
* Get any user defined searchable fields labels that
* exist . Allows overriding of default field names in the form
* interface actually presented to the user .
2008-08-11 01:17:51 +02:00
*
2008-08-09 08:53:26 +02:00
* The reason for keeping this separate from searchable_fields ,
2008-08-11 01:17:51 +02:00
* which would be a logical place for this functionality , is to
2008-08-09 08:53:26 +02:00
* avoid bloating and complicating the configuration array . Currently
* much of this system is based on sensible defaults , and this property
* would generally only be set in the case of more complex relationships
* between data object being required in the search interface .
2008-08-11 01:17:51 +02:00
*
* Generates labels based on name of the field itself , if no static property
2008-10-03 20:38:52 +02:00
* { @ link self :: field_labels } exists .
2008-08-11 01:17:51 +02:00
*
2008-10-03 20:38:52 +02:00
* @ uses $field_labels
* @ uses FormField :: name_to_label ()
2009-04-29 02:07:39 +02:00
*
* @ param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
2008-10-03 20:38:52 +02:00
*
2010-04-23 02:11:41 +02:00
* @ return array | string Array of all element labels if no argument given , otherwise the label of the field
2008-08-09 08:53:26 +02:00
*/
2009-04-29 02:07:39 +02:00
public function fieldLabels ( $includerelations = true ) {
2012-04-13 18:38:23 +02:00
$cacheKey = $this -> class . '_' . $includerelations ;
2008-11-02 21:01:49 +01:00
2012-04-13 18:38:23 +02:00
if ( ! isset ( self :: $_cache_field_labels [ $cacheKey ])) {
$customLabels = $this -> stat ( 'field_labels' );
$autoLabels = array ();
// get all translated static properties as defined in i18nCollectStatics()
$ancestry = ClassInfo :: ancestry ( $this -> class );
$ancestry = array_reverse ( $ancestry );
if ( $ancestry ) foreach ( $ancestry as $ancestorClass ) {
if ( $ancestorClass == 'ViewableData' ) break ;
$types = array (
'db' => ( array ) Config :: inst () -> get ( $ancestorClass , 'db' , Config :: UNINHERITED )
);
if ( $includerelations ){
$types [ 'has_one' ] = ( array ) singleton ( $ancestorClass ) -> uninherited ( 'has_one' , true );
$types [ 'has_many' ] = ( array ) singleton ( $ancestorClass ) -> uninherited ( 'has_many' , true );
$types [ 'many_many' ] = ( array ) singleton ( $ancestorClass ) -> uninherited ( 'many_many' , true );
}
foreach ( $types as $type => $attrs ) {
foreach ( $attrs as $name => $spec ) {
// var_dump("{$ancestorClass}.{$type}_{$name}");
$autoLabels [ $name ] = _t ( " { $ancestorClass } . { $type } _ { $name } " , FormField :: name_to_label ( $name ));
}
}
}
2008-10-28 04:03:16 +01:00
2012-04-13 18:38:23 +02:00
$labels = array_merge (( array ) $autoLabels , ( array ) $customLabels );
$this -> extend ( 'updateFieldLabels' , $labels );
self :: $_cache_field_labels [ $cacheKey ] = $labels ;
}
return self :: $_cache_field_labels [ $cacheKey ];
2008-10-03 20:38:52 +02:00
}
/**
* Get a human - readable label for a single field ,
* see { @ link fieldLabels ()} for more details .
*
* @ uses fieldLabels ()
* @ uses FormField :: name_to_label ()
*
* @ param string $name Name of the field
* @ return string Label of the field
*/
public function fieldLabel ( $name ) {
$labels = $this -> fieldLabels ();
return ( isset ( $labels [ $name ])) ? $labels [ $name ] : FormField :: name_to_label ( $name );
2008-08-09 08:53:26 +02:00
}
2008-08-11 01:17:51 +02:00
2008-08-09 06:06:52 +02:00
/**
* Get the default summary fields for this object .
2008-08-09 07:04:15 +02:00
*
2008-08-09 06:06:52 +02:00
* @ todo use the translation apparatus to return a default field selection for the language
2008-08-09 07:04:15 +02:00
*
2008-08-09 06:06:52 +02:00
* @ return array
*/
2008-08-27 06:11:58 +02:00
public function summaryFields (){
2008-08-09 06:06:52 +02:00
$fields = $this -> stat ( 'summary_fields' );
2008-08-11 01:17:51 +02:00
2008-08-09 08:29:50 +02:00
// if fields were passed in numeric array,
// convert to an associative array
if ( $fields && array_key_exists ( 0 , $fields )) {
$fields = array_combine ( array_values ( $fields ), array_values ( $fields ));
}
2008-08-11 01:17:51 +02:00
2008-08-09 06:06:52 +02:00
if ( ! $fields ) {
$fields = array ();
2008-08-09 08:29:50 +02:00
// try to scaffold a couple of usual suspects
2008-08-09 06:38:44 +02:00
if ( $this -> hasField ( 'Name' )) $fields [ 'Name' ] = 'Name' ;
2009-02-02 00:49:53 +01:00
if ( $this -> hasDataBaseField ( 'Title' )) $fields [ 'Title' ] = 'Title' ;
2008-08-09 06:38:44 +02:00
if ( $this -> hasField ( 'Description' )) $fields [ 'Description' ] = 'Description' ;
2008-08-26 03:45:52 +02:00
if ( $this -> hasField ( 'FirstName' )) $fields [ 'FirstName' ] = 'First Name' ;
2008-08-09 06:06:52 +02:00
}
2008-08-26 03:45:52 +02:00
$this -> extend ( " updateSummaryFields " , $fields );
2008-10-03 18:01:51 +02:00
// Final fail-over, just list ID field
if ( ! $fields ) $fields [ 'ID' ] = 'ID' ;
2012-08-10 00:14:02 +02:00
// Localize fields (if possible)
2012-08-29 13:42:29 +02:00
foreach ( $this -> fieldLabels ( false ) as $name => $label ) {
if ( isset ( $fields [ $name ])) $fields [ $name ] = $label ;
}
2008-10-03 18:01:51 +02:00
2008-08-09 06:06:52 +02:00
return $fields ;
}
2008-08-09 07:04:15 +02:00
2008-08-09 06:06:52 +02:00
/**
* Defines a default list of filters for the search context .
2008-08-09 07:04:15 +02:00
*
2008-08-09 06:06:52 +02:00
* If a filter class mapping is defined on the data object ,
* it is constructed here . Otherwise , the default filter specified in
* { @ link DBField } is used .
2008-08-09 07:04:15 +02:00
*
2008-08-09 06:06:52 +02:00
* @ todo error handling / type checking for valid FormField and SearchFilter subclasses ?
2008-08-09 07:04:15 +02:00
*
2008-08-09 06:06:52 +02:00
* @ return array
*/
public function defaultSearchFilters () {
$filters = array ();
2012-07-22 18:11:35 +02:00
2008-08-11 01:29:30 +02:00
foreach ( $this -> searchableFields () as $name => $spec ) {
$filterClass = $spec [ 'filter' ];
2012-07-22 18:11:35 +02:00
if ( $spec [ 'filter' ] instanceof SearchFilter ) {
$filters [ $name ] = $spec [ 'filter' ];
} else {
$class = $spec [ 'filter' ];
if ( ! is_subclass_of ( $spec [ 'filter' ], 'SearchFilter' )) {
$class = 'PartialMatchFilter' ;
}
$filters [ $name ] = new $class ( $name );
2008-08-11 05:03:52 +02:00
}
2008-08-06 05:43:48 +02:00
}
2012-07-22 18:11:35 +02:00
2008-08-09 06:06:52 +02:00
return $filters ;
2008-08-06 05:43:48 +02:00
}
2008-08-09 07:04:15 +02:00
2007-08-16 08:32:49 +02:00
/**
2008-08-09 07:04:15 +02:00
* @ return boolean True if the object is in the database
*/
public function isInDB () {
return is_numeric ( $this -> ID ) && $this -> ID > 0 ;
}
2008-08-12 04:51:33 +02:00
/*
* @ ignore
*/
private static $subclass_access = true ;
/**
* Temporarily disable subclass access in data object qeur
*/
2012-09-19 12:07:39 +02:00
public static function disable_subclass_access () {
2008-08-12 04:51:33 +02:00
self :: $subclass_access = false ;
}
2012-09-19 12:07:39 +02:00
public static function enable_subclass_access () {
2008-08-12 04:51:33 +02:00
self :: $subclass_access = true ;
}
2007-07-19 12:40:28 +02:00
//-------------------------------------------------------------------------------------------//
/**
* Database field definitions .
* This is a map from field names to field type . The field
2007-08-16 08:32:49 +02:00
* type should be a class that extends .
2007-07-19 12:40:28 +02:00
* @ var array
2011-12-22 03:31:56 +01:00
* @ config
2007-07-19 12:40:28 +02:00
*/
public static $db = null ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Use a casting object for a field . This is a map from
* field name to class name of the casting object .
* @ var array
*/
public static $casting = array (
API CHANGE: Renamed conflicting classes to have an "SS_" namespace, and renamed existing "SS" namespace to "SS_". The affected classes are: HTTPRequest, HTTPResponse, Query, Database, SSBacktrace, SSCli, SSDatetime, SSDatetimeTest, SSLog, SSLogTest, SSLogEmailWriter, SSLogErrorEmailFormatter, SSLogErrorFileFormatter, SSLogFileWriter and SSZendLog.
MINOR: Replaced usage of renamed classes with the new namespaced name.
From: Andrew Short <andrewjshort@gmail.com>
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@90075 467b73ca-7a2a-4603-9d3b-597d59a354a9
2009-10-26 04:06:31 +01:00
" LastEdited " => " SS_Datetime " ,
" Created " => " SS_Datetime " ,
2008-08-11 04:25:44 +02:00
" Title " => 'Text' ,
2007-07-19 12:40:28 +02:00
);
2009-05-19 05:55:14 +02:00
/**
* Specify custom options for a CREATE TABLE call .
* Can be used to specify a custom storage engine for specific database table .
* All options have to be keyed for a specific database implementation ,
API CHANGE: Renamed conflicting classes to have an "SS_" namespace, and renamed existing "SS" namespace to "SS_". The affected classes are: HTTPRequest, HTTPResponse, Query, Database, SSBacktrace, SSCli, SSDatetime, SSDatetimeTest, SSLog, SSLogTest, SSLogEmailWriter, SSLogErrorEmailFormatter, SSLogErrorFileFormatter, SSLogFileWriter and SSZendLog.
MINOR: Replaced usage of renamed classes with the new namespaced name.
From: Andrew Short <andrewjshort@gmail.com>
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@90075 467b73ca-7a2a-4603-9d3b-597d59a354a9
2009-10-26 04:06:31 +01:00
* identified by their class name ( extending from { @ link SS_Database }) .
2009-05-19 05:55:14 +02:00
*
* < code >
* array (
* 'MySQLDatabase' => 'ENGINE=MyISAM'
* )
* </ code >
2009-05-19 06:44:33 +02:00
*
* Caution : This API is experimental , and might not be
* included in the next major release . Please use with care .
2009-05-19 05:55:14 +02:00
*
* @ var array
*/
static $create_table_options = array (
2011-10-29 02:34:45 +02:00
'MySQLDatabase' => 'ENGINE=InnoDB'
2009-05-19 05:55:14 +02:00
);
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* If a field is in this array , then create a database index
* on that field . This is a map from fieldname to index type .
API CHANGE: Renamed conflicting classes to have an "SS_" namespace, and renamed existing "SS" namespace to "SS_". The affected classes are: HTTPRequest, HTTPResponse, Query, Database, SSBacktrace, SSCli, SSDatetime, SSDatetimeTest, SSLog, SSLogTest, SSLogEmailWriter, SSLogErrorEmailFormatter, SSLogErrorFileFormatter, SSLogFileWriter and SSZendLog.
MINOR: Replaced usage of renamed classes with the new namespaced name.
From: Andrew Short <andrewjshort@gmail.com>
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@90075 467b73ca-7a2a-4603-9d3b-597d59a354a9
2009-10-26 04:06:31 +01:00
* See { @ link SS_Database -> requireIndex ()} and custom subclasses for details on the array notation .
2009-03-12 12:11:35 +01:00
*
2007-07-19 12:40:28 +02:00
* @ var array
*/
public static $indexes = null ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Inserts standard column - values when a DataObject
* is instanciated . Does not insert default records { @ see $default_records } .
2009-02-02 00:49:53 +01:00
* This is a map from fieldname to default value .
2008-09-23 02:17:58 +02:00
*
* - If you would like to change a default value in a sub - class , just specify it .
* - If you would like to disable the default value given by a parent class , set the default value to 0 , '' , or false in your
* subclass . Setting it to null won ' t work .
*
2007-07-19 12:40:28 +02:00
* @ var array
*/
public static $defaults = null ;
/**
2007-09-15 01:10:25 +02:00
* Multidimensional array which inserts default data into the database
2007-07-19 12:40:28 +02:00
* on a db / build - call as long as the database - table is empty . Please use this only
* for simple constructs , not for SiteTree - Objects etc . which need special
* behaviour such as publishing and ParentNodes .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* Example :
* array (
* array ( 'Title' => " DefaultPage1 " , 'PageTitle' => 'page1' ),
* array ( 'Title' => " DefaultPage2 " )
* ) .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ var array
*/
public static $default_records = null ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
2009-10-24 01:27:51 +02:00
* One - to - zero relationship defintion . This is a map of component name to data type . In order to turn this into a
* true one - to - one relationship you can add a { @ link DataObject :: $belongs_to } relationship on the child class .
*
* Note that you cannot have a has_one and belongs_to relationship with the same name .
*
2007-07-19 12:40:28 +02:00
* @ var array
*/
public static $has_one = null ;
2009-10-24 01:27:51 +02:00
/**
* A meta - relationship that allows you to define the reverse side of a { @ link DataObject :: $has_one } .
*
* This does not actually create any data structures , but allows you to query the other object in a one - to - one
* relationship from the child object . If you have multiple belongs_to links to another object you can use the
* syntax " ClassName.HasOneName " to specify which foreign has_one key on the other object to use .
*
* Note that you cannot have a has_one and belongs_to relationship with the same name .
*
* @ var array
*/
public static $belongs_to ;
2007-07-19 12:40:28 +02:00
/**
2009-10-24 01:26:10 +02:00
* This defines a one - to - many relationship . It is a map of component name to the remote data class .
2008-08-09 07:04:15 +02:00
*
2009-10-24 01:26:10 +02:00
* This relationship type does not actually create a data structure itself - you need to define a matching $has_one
* relationship on the child class . Also , if the $has_one relationship on the child class has multiple links to this
* class you can use the syntax " ClassName.HasOneRelationshipName " in the remote data class definition to show
* which foreign key to use .
2008-08-09 07:04:15 +02:00
*
2007-07-19 12:40:28 +02:00
* @ var array
*/
public static $has_many = null ;
/**
* many - many relationship definitions .
* This is a map from component name to data type .
* @ var array
*/
public static $many_many = null ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Extra fields to include on the connecting many - many table .
* This is a map from field name to field type .
2009-02-03 22:07:32 +01:00
*
* Example code :
* < code >
* public static $many_many_extraFields = array (
* 'Members' => array (
* 'Role' => 'Varchar(100)'
* )
* );
* </ code >
*
2007-07-19 12:40:28 +02:00
* @ var array
*/
public static $many_many_extraFields = null ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* The inverse side of a many - many relationship .
* This is a map from component name to data type .
* @ var array
*/
public static $belongs_many_many = null ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* The default sort expression . This will be inserted in the ORDER BY
* clause of a SQL query if no other sort expression is provided .
* @ var string
*/
public static $default_sort = null ;
2008-08-09 07:04:15 +02:00
2008-08-09 06:06:52 +02:00
/**
* Default list of fields that can be scaffolded by the ModelAdmin
* search interface .
2008-08-09 07:04:15 +02:00
*
2008-08-11 01:29:30 +02:00
* Overriding the default filter , with a custom defined filter :
2008-08-09 06:06:52 +02:00
* < code >
* static $searchable_fields = array (
2008-08-11 01:29:30 +02:00
* " Name " => " PartialMatchFilter "
2008-08-09 06:06:52 +02:00
* );
* </ code >
2008-08-11 01:29:30 +02:00
*
* Overriding the default form fields , with a custom defined field .
* The 'filter' parameter will be generated from { @ link DBField :: $default_search_filter_class } .
* The 'title' parameter will be generated from { @ link DataObject -> fieldLabels ()} .
2008-08-09 06:06:52 +02:00
* < code >
* static $searchable_fields = array (
2008-08-11 01:29:30 +02:00
* " Name " => array (
* " field " => " TextField "
* )
2008-08-09 06:06:52 +02:00
* );
* </ code >
2008-08-09 07:04:15 +02:00
*
2008-08-11 01:29:30 +02:00
* Overriding the default form field , filter and title :
2008-08-09 06:06:52 +02:00
* < code >
* static $searchable_fields = array (
2008-08-11 01:29:30 +02:00
* " Organisation.ZipCode " => array (
* " field " => " TextField " ,
* " filter " => " PartialMatchFilter " ,
* " title " => 'Organisation ZIP'
* )
2008-08-09 06:06:52 +02:00
* );
2008-08-09 07:04:15 +02:00
* </ code >
2008-08-09 06:06:52 +02:00
*/
public static $searchable_fields = null ;
2008-08-09 08:53:26 +02:00
/**
* User defined labels for searchable_fields , used to override
* default display in the search form .
*/
2008-08-11 01:29:30 +02:00
public static $field_labels = null ;
2008-08-11 01:17:51 +02:00
2008-08-09 06:06:52 +02:00
/**
* Provides a default list of fields to be used by a 'summary'
* view of this object .
*/
public static $summary_fields = null ;
2008-10-29 22:07:17 +01:00
2009-04-29 01:40:35 +02:00
/**
* Provides a list of allowed methods that can be called via RESTful api .
*/
public static $allowed_actions = null ;
2008-10-29 22:07:17 +01:00
/**
* Collect all static properties on the object
* which contain natural language , and need to be translated .
* The full entity name is composed from the class name and a custom identifier .
*
* @ return array A numerical array which contains one or more entities in array - form .
* Each numeric entity array contains the " arguments " for a _t () call as array values :
* $entity , $string , $priority , $context .
*/
public function provideI18nEntities () {
$entities = array ();
$entities [ " { $this -> class } .SINGULARNAME " ] = array (
2008-11-01 14:23:38 +01:00
$this -> singular_name (),
2012-04-14 00:16:22 +02:00
2008-10-29 22:07:17 +01:00
'Singular name of the object, used in dropdowns and to generally identify a single object in the interface'
);
2008-11-01 14:23:38 +01:00
2008-10-29 22:07:17 +01:00
$entities [ " { $this -> class } .PLURALNAME " ] = array (
2008-11-01 14:23:38 +01:00
$this -> plural_name (),
2012-04-14 00:16:22 +02:00
2008-10-29 22:07:17 +01:00
'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the interface'
);
return $entities ;
}
2009-09-17 02:09:23 +02:00
2009-10-23 03:29:55 +02:00
/**
* Returns true if the given method / parameter has a value
* ( Uses the DBField :: hasValue if the parameter is a database field )
2010-10-04 06:21:06 +02:00
*
* @ param string $field The field name
* @ param array $arguments
* @ param bool $cache
2009-10-23 03:29:55 +02:00
* @ return boolean
*/
2012-09-19 12:07:39 +02:00
public function hasValue ( $field , $arguments = null , $cache = true ) {
2010-10-04 06:21:06 +02:00
$obj = $this -> dbObject ( $field );
if ( $obj ) {
2012-04-11 04:48:06 +02:00
return $obj -> exists ();
2009-10-23 03:29:55 +02:00
} else {
2010-10-04 06:21:06 +02:00
return parent :: hasValue ( $field , $arguments , $cache );
2009-10-23 03:29:55 +02:00
}
2010-10-04 06:21:06 +02:00
}
2007-09-16 18:40:57 +02:00
2012-03-27 06:04:11 +02:00
}