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
*
* 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 ;
*
* public function canView ( $member = false ) {
* return Permission :: check ( 'ARTICLE_VIEW' );
* }
* public function canEdit ( $member = false ) {
* return Permission :: check ( 'ARTICLE_EDIT' );
* }
* public function canDelete () {
* return Permission :: check ( 'ARTICLE_DELETE' );
* }
* public function canCreate () {
* return Permission :: check ( 'ARTICLE_CREATE' );
* }
* public function providePermissions () {
* 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 ;
*
* public function canView ( $member = false ) {
* if ( ! $member ) $member = Member :: currentUser ();
* return $member -> inGroup ( 'Subscribers' );
* }
* public function canEdit ( $member = false ) {
* 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 >
*
2008-02-25 03:10:37 +01:00
* @ package sapphire
* @ subpackage model
2007-07-19 12:40:28 +02:00
*/
2008-11-01 14:26:08 +01:00
class DataObject extends ViewableData implements DataObjectInterface , i18nEntityProvider {
2007-07-19 12:40:28 +02:00
/**
* Data stored in this objects database record . An array indexed
* by fieldname .
* @ 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 .
* @ var array
*/
protected $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 ;
/**
* The one - to - one , one - to - many and many - to - one components
* indexed by component name .
* @ var array
*/
protected $components ;
2008-08-09 07:04:15 +02:00
2007-07-19 12:40:28 +02:00
/**
* True if this DataObject has been destroyed .
* @ var boolean
*/
public $destroyed = false ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Human - readable singular name .
* @ var string
*/
static $singular_name = null ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Human - readable pluaral name
* @ var string
*/
static $plural_name = null ;
2008-08-09 07:04:15 +02:00
2008-03-31 01:18:04 +02:00
/**
* Allow API access to this object ?
* @ todo Define the options that can be set here
*/
static $api_access = false ;
2007-09-15 01:10:25 +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 .
*/
function __construct ( $record = null , $isSingleton = false ) {
// Set the fields data.
if ( ! $record ) {
$record = array ( " ID " => 0 );
}
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,
2008-08-09 07:04:15 +02:00
taken straight from the database . Perhaps you should use DataObject :: get_one instead ? " , E_USER_WARNING);
2007-07-19 12:40:28 +02:00
$record = null ;
}
$this -> record = $this -> original = $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' ]);
}
parent :: __construct ();
// 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 ();
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Destroy all of this objects dependant objects .
* You ' ll need to call this to get the memory of an object that has components or extensions freed .
*/
function destroy () {
$this -> extension_instances = null ;
$this -> components = null ;
$this -> destroyed = true ;
2008-08-11 01:03:35 +02:00
$this -> flushCache ();
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 .
2008-02-25 03:10:37 +01:00
* Caution : Doesn ' t duplicate 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 .
*/
2007-08-16 08:32:49 +02:00
function duplicate ( $doWrite = true ) {
2007-07-19 12:40:28 +02:00
$className = $this -> class ;
$clone = new $className ( $this -> record );
$clone -> ID = 0 ;
2007-08-16 08:32:49 +02:00
if ( $doWrite ) $clone -> write ();
2007-07-19 12:40:28 +02:00
return $clone ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Set the ClassName attribute ; $this -> class is also updated .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $className The new ClassName attribute
*/
function setClassName ( $className ) {
$this -> class = trim ( $className );
$this -> setField ( " ClassName " , $className );
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Create a new instance of a different class from this object ' s record
* 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
2007-07-19 12:40:28 +02:00
* record .
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 .
*/
function newClassInstance ( $newClassName ) {
$newRecord = $this -> record ;
//$newRecord['RecordClassName'] = $newRecord['ClassName'] = $newClassName;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
$newInstance = new $newClassName ( $newRecord );
$newInstance -> setClassName ( $newClassName );
$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
*/
2007-07-19 12:40:28 +02:00
function defineMethods () {
if ( $this -> class == 'DataObject' ) return ;
2007-08-16 08:32:49 +02:00
parent :: defineMethods ();
// Define the extra db fields
if ( $this -> extension_instances ) foreach ( $this -> extension_instances as $i => $instance ) {
2007-07-19 12:40:28 +02:00
$instance -> loadExtraDBFields ();
}
2008-08-09 07:04:15 +02:00
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' );
}
}
}
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
2007-08-23 07:47:54 +02:00
public function isEmpty (){
$isEmpty = true ;
if ( $this -> record ){
foreach ( $this -> record as $k => $v ){
if ( $k != " ID " ){
$isEmpty = $isEmpty && ! $v ;
}
}
}
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
*/
function singular_name () {
$name = $this -> stat ( 'singular_name' );
if ( ! $name ) {
2007-09-15 01:10:25 +02:00
$name = ucwords ( trim ( strtolower ( ereg_replace ( '([A-Z])' , ' \\1' , $this -> class ))));
}
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
*
* NOTE :
* It uses as default text if no translation found the $add_action when
* defined or else the default text is singular_name ()
*
* Translating string is in the form :
* $this -> class . SINGULARNAME
* Example :
* Page . SINGULARNAME
*
* @ return string User friendly translated singular name of this DataObject
*/
2008-10-29 22:07:17 +01:00
function i18n_singular_name () {
2008-08-09 07:04:15 +02:00
$name = ( ! empty ( $this -> add_action )) ? $this -> add_action : $this -> singular_name ();
2008-02-25 03:10:37 +01:00
return _t ( $this -> class . '.SINGULARNAME' , $name );
}
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
*/
function plural_name () {
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
*/
function i18n_plural_name ()
{
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 >
* public function getTitle () {
* return " { $this -> StreetNumber } { $this -> StreetName } { $this -> City } " ;
* }
* </ code >
*
* @ usedby { @ link DataObjectSet -> toDropDownMap ()}
*
* @ return string
*/
public function getTitle () {
if ( $this -> hasField ( 'Title' )) return $this -> getField ( 'Title' );
if ( $this -> hasField ( 'Name' )) return $this -> getField ( 'Name' );
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 () {
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
$leftData = $leftObj -> customDatabaseFields ();
$rightData = $rightObj -> customDatabaseFields ();
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 .
* Doesn ' t write to the database .
*/
public function forceChange () {
foreach ( $this -> record as $fieldName => $fieldVal )
2008-08-09 07:04:15 +02:00
$this -> changed [ $fieldName ] = 1 ;
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
* DataObject sub - classes to specify custom validation .
*
* 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 () {
return new ValidationResult ();
}
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 .
2007-07-19 12:40:28 +02:00
*/
protected function onBeforeWrite () {
$this -> brokenOnWrite = false ;
2007-09-16 18:40:57 +02:00
2007-08-16 08:32:49 +02:00
$dummy = null ;
$this -> extend ( 'augmentBeforeWrite' , $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 !
*/
protected function onAfterWrite () {
$dummy = null ;
$this -> extend ( 'augmentAfterWrite' , $dummy );
}
2007-07-19 12:40:28 +02:00
/**
* Used by onBeforeWrite () to ensure child classes call parent :: onBeforeWrite ()
* @ var boolean
*/
protected $brokenOnWrite = false ;
2007-09-15 01:10:25 +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 !
*/
protected function onBeforeDelete () {
$this -> brokenOnDelete = false ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Used by onBeforeDelete () to ensure child classes call parent :: onBeforeDelete ()
* @ var boolean
*/
protected $brokenOnDelete = false ;
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 .
*/
public function populateDefaults () {
$classes = array_reverse ( ClassInfo :: ancestry ( $this ));
foreach ( $classes as $class ) {
$singleton = ( $class == $this -> class ) ? $this : singleton ( $class );
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
$defaults = $singleton -> stat ( 'defaults' );
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 ;
}
}
}
/**
* 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 .
* - Calls to { @ link DataObjectLog } can be used to see everything that ' s been changed .
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 ;
2008-04-26 08:31:52 +02:00
$valid = $this -> validate ();
if ( ! $valid -> valid ()) {
2008-10-08 22:42:09 +02:00
throw new ValidationException ( $valid , " Validation error writing a $this->class object: " . $valid -> message () . " . Object not written. " , E_USER_WARNING );
2008-04-26 08:31:52 +02:00
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 ) {
if ( is_object ( $v ) && $v -> isChanged ()) {
$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 ) {
if ( ClassInfo :: hasTable ( $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
}
2008-10-28 02:23:41 +01: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
2008-08-11 02:14:48 +02:00
if (( ! isset ( $this -> record [ 'ID' ]) || ! $this -> record [ 'ID' ]) && isset ( $ancestry [ 0 ])) {
2008-02-25 03:10:37 +01:00
$baseTable = $ancestry [ 0 ];
2007-09-16 18:40:57 +02:00
2008-02-25 03:10:37 +01:00
DB :: query ( " INSERT INTO ` { $baseTable } ` SET Created = NOW() " );
$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 );
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 ) {
2008-10-01 02:55:25 +02:00
$fieldObj = DBField :: create ( 'Varchar' , $this -> record [ $fieldName ], $fieldName );
2008-08-15 05:08:03 +02:00
}
2008-08-09 07:04:15 +02:00
2008-08-11 01:17:51 +02:00
// CompositeDBFields handle their own value storage; regular fields need to be
// re-populated from the database
if ( ! $fieldObj instanceof CompositeDBField ) {
$fieldObj -> setValue ( $this -> record [ $fieldName ], $this -> record );
}
2008-08-09 05:54:55 +02:00
$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 ) {
$manipulation [ $class ][ 'fields' ][ " LastEdited " ] = " now() " ;
if ( $dbCommand == 'insert' ) {
$manipulation [ $class ][ 'fields' ][ " Created " ] = " now() " ;
//echo "<li>$this->class - " .get_class($this);
$manipulation [ $class ][ 'fields' ][ " ClassName " ] = " ' $this->class ' " ;
}
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
if ( ClassInfo :: hasTable ( $class )) {
$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 );
// 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' ;
}
2008-08-12 04:51:33 +02:00
2008-02-25 03:10:37 +01:00
DB :: manipulate ( $manipulation );
2007-07-19 12:40:28 +02:00
2008-02-25 03:10:37 +01:00
if ( isset ( $isNewRecord ) && $isNewRecord ) {
DataObjectLog :: addedObject ( $this );
} else {
DataObjectLog :: changedObject ( $this );
}
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 /> " ;
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' ])) {
$this -> record [ 'Created' ] = date ( 'Y-m-d H:i:s' );
}
$this -> record [ 'LastEdited' ] = date ( 'Y-m-d H:i:s' );
}
2008-02-25 02:06:39 +01:00
2007-07-19 12:40:28 +02:00
// Write ComponentSets as necessary
2008-04-10 03:43:31 +02:00
if ( $writeComponents ) {
$this -> writeComponents ( true );
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 $this -> record [ 'ID' ];
}
2007-09-15 01:10:25 +02:00
2007-09-16 18:40:57 +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 .
*
* @ 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
/**
* Perform a write without affecting the version table .
* On objects without versioning .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return int The ID of the record
*/
public function writeWithoutVersion () {
$this -> changed [ 'Version' ] = 1 ;
if ( ! isset ( $this -> record [ 'Version' ])) {
$this -> record [ 'Version' ] = - 1 ;
}
return $this -> write ();
}
/**
* Delete this data object .
* $this -> onBeforeDelete () gets called .
* Note that in Versioned objects , both Stage and Live will be deleted .
*/
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 );
}
foreach ( $this -> getClassAncestry () as $ancestor ) {
if ( ClassInfo :: hastable ( $ancestor )) {
$sql = new SQLQuery ();
$sql -> delete = true ;
$sql -> from [ $ancestor ] = " ` $ancestor ` " ;
$sql -> where [] = " ID = $this->ID " ;
$this -> extend ( 'augmentSQL' , $sql );
$sql -> execute ();
}
}
$this -> OldID = $this -> ID ;
$this -> ID = 0 ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
DataObjectLog :: deletedObject ( $this );
}
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 );
}
}
/**
* A cache used by getClassAncestry ()
* @ var array
*/
protected static $ancestry ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* 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 () {
if ( ! isset ( DataObject :: $ancestry [ $this -> class ])) {
DataObject :: $ancestry [ $this -> class ] = array ( $this -> class );
while (( $class = get_parent_class ( DataObject :: $ancestry [ $this -> class ][ 0 ])) != " DataObject " ) {
array_unshift ( DataObject :: $ancestry [ $this -> class ], $class );
}
}
return DataObject :: $ancestry [ $this -> class ];
}
/**
* 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 ];
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
if ( $componentClass = $this -> has_one ( $componentName )) {
$childID = $this -> getField ( $componentName . 'ID' );
if ( $childID && is_numeric ( $childID )) {
$component = DataObject :: get_by_id ( $componentClass , $childID );
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
// If no component exists, create placeholder object
if ( ! isset ( $component )) {
$component = $this -> createComponent ( $componentName );
// We may have had an orphaned ID that needs to be cleaned up
$this -> setField ( $componentName . 'ID' , 0 );
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
// If no component exists, create placeholder object
if ( ! $component ) {
$component = $this -> createComponent ( $componentName );
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
$this -> components [ $componentName ] = $component ;
return $component ;
} else {
user_error ( " DataObject::getComponent(): Unknown 1-to-1 component ' $componentName ' on class ' $this->class ' " , E_USER_ERROR );
}
}
2008-08-11 01:17:51 +02:00
2007-07-19 12:40:28 +02:00
/**
* A cache used by component getting classes
* @ var array
*/
2008-08-09 07:04:15 +02:00
protected $componentCache ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Returns a one - to - many component , as a ComponentSet .
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
*
2007-07-19 12:40:28 +02:00
* @ return ComponentSet The components of the one - to - many relationship .
*/
2008-02-25 03:10:37 +01:00
public function getComponents ( $componentName , $filter = " " , $sort = " " , $join = " " , $limit = " " ) {
$result = null ;
2008-08-09 07:04:15 +02:00
2008-02-25 03:10:37 +01:00
$sum = md5 ( " { $filter } _ { $sort } _ { $join } _ { $limit } " );
if ( isset ( $this -> componentCache [ $componentName . '_' . $sum ]) && false != $this -> componentCache [ $componentName . '_' . $sum ]) {
return $this -> componentCache [ $componentName . '_' . $sum ];
}
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
2007-07-19 12:40:28 +02:00
$joinField = $this -> getComponentJoinField ( $componentName );
2008-08-09 07:04:15 +02:00
2008-02-25 03:10:37 +01:00
if ( $this -> isInDB ()) { //Check to see whether we should query the db
2008-08-09 07:57:44 +02:00
$query = $this -> getComponentsQuery ( $componentName , $filter , $sort , $join , $limit );
$result = $this -> buildDataObjectSet ( $query -> execute (), 'ComponentSet' , $query , $componentClass );
if ( $result ) $result -> parseQueryLimit ( $query );
2008-02-25 03:10:37 +01:00
}
2008-08-09 07:04:15 +02:00
2007-07-19 12:40:28 +02:00
if ( ! $result ) {
2008-02-25 03:10:37 +01:00
// If this record isn't in the database, then we want to hold onto this specific ComponentSet,
// because it's the only copy of the data that we have.
2007-07-19 12:40:28 +02:00
$result = new ComponentSet ();
2008-02-25 02:06:39 +01:00
$this -> setComponent ( $componentName . '_' . $sum , $result );
}
2008-08-09 07:04:15 +02:00
2008-02-25 03:10:37 +01:00
$result -> setComponentInfo ( " 1-to-many " , $this , null , null , $componentClass , $joinField );
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
* Use { @ link DataObjectSet -> setComponentInfo ()} to attach metadata to the
* resultset you ' re building with this query .
* Use { @ link DataObject -> buildDataObjectSet ()} to build a set out of the { @ link SQLQuery }
* object , and pass " ComponentSet " as a $containerClass .
*
* @ 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 );
}
$joinField = $this -> getComponentJoinField ( $componentName );
2008-08-11 01:17:51 +02:00
2008-08-09 07:57:44 +02:00
$id = $this -> getField ( " ID " );
// get filter
$combinedFilter = " $joinField = ' $id ' " ;
if ( $filter ) $combinedFilter .= " AND { $filter } " ;
return singleton ( $componentClass ) -> extendedSQL ( $combinedFilter , $sort , $limit , $join );
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Tries to find the db - key for storing a relation ( defaults to " ParentID " if no relation is found ) .
* The iteration is necessary because the most specific class does not always have a database - table .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $componentName Name of one to many component
2007-09-15 01:10:25 +02:00
*
* @ return string Fieldname for the parent - relation
2007-07-19 12:40:28 +02:00
*/
public function getComponentJoinField ( $componentName ) {
if ( ! $componentClass = $this -> has_many ( $componentName )) {
2008-08-09 07:57:44 +02:00
user_error ( " DataObject::getComponents(): Unknown 1-to-many component ' $componentName ' on class ' $this->class ' " , E_USER_ERROR );
2007-07-19 12:40:28 +02:00
}
$componentObj = singleton ( $componentClass );
// get has-one relations
$reversedComponentRelations = array_flip ( $componentObj -> has_one ());
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
// get all parentclasses for the current class which have tables
$allClasses = ClassInfo :: ancestry ( $this -> class );
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
// use most specific relation by default (turn around order)
$allClasses = array_reverse ( $allClasses );
2007-09-15 01:10:25 +02:00
// traverse up through all classes with database-tables, starting with the most specific
// (mostly the classname of the calling DataObject)
2007-07-19 12:40:28 +02:00
foreach ( $allClasses as $class ) {
// if this class does a "has-one"-representation, use it
2007-09-15 01:31:08 +02:00
if ( isset ( $reversedComponentRelations [ $class ]) && false != $reversedComponentRelations [ $class ]) {
2007-07-19 12:40:28 +02:00
$joinField = $reversedComponentRelations [ $class ] . 'ID' ;
break ;
}
}
2007-07-24 01:00:11 +02:00
if ( ! isset ( $joinField )) {
2007-07-19 12:40:28 +02:00
$joinField = 'ParentID' ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
return $joinField ;
}
2007-09-15 01:10:25 +02:00
2008-08-09 07:04:15 +02:00
/**
* Sets the component of a relationship .
*
* @ param string $componentName Name of the component
* @ param DataObject | ComponentSet $componentValue Value of the component
*/
public function setComponent ( $componentName , $componentValue ) {
$this -> componentCache [ $componentName ] = $componentValue ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Returns a many - to - many component , as a ComponentSet .
* @ param string $componentName Name of the many - many component
* @ return ComponentSet 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 = " " ) {
$sum = md5 ( " { $filter } _ { $sort } _ { $join } _ { $limit } " );
2008-08-09 07:04:15 +02:00
if ( isset ( $this -> componentCache [ $componentName . '_' . $sum ]) && false != $this -> componentCache [ $componentName . '_' . $sum ]) {
return $this -> componentCache [ $componentName . '_' . $sum ];
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
list ( $parentClass , $componentClass , $parentField , $componentField , $table ) = $this -> many_many ( $componentName );
2008-08-09 07:57:44 +02:00
// Join expression is done on SiteTree.ID even if we link to Page; it helps work around
// database inconsistencies
$componentBaseClass = ClassInfo :: baseDataClass ( $componentClass );
2008-08-11 01:17:51 +02:00
2007-07-19 12:40:28 +02:00
if ( $this -> ID && is_numeric ( $this -> ID )) {
2008-08-11 01:17:51 +02:00
2007-07-19 12:40:28 +02:00
if ( $componentClass ) {
2008-08-09 07:57:44 +02:00
$query = $this -> getManyManyComponentsQuery ( $componentName , $filter , $sort , $join , $limit );
2007-07-19 12:40:28 +02:00
$records = $query -> execute ();
$result = $this -> buildDataObjectSet ( $records , " ComponentSet " , $query , $componentBaseClass );
2008-07-11 02:13:17 +02:00
if ( $result ) $result -> parseQueryLimit ( $query ); // for pagination support
2007-07-19 12:40:28 +02:00
if ( ! $result ) {
$result = new ComponentSet ();
}
}
} else {
$result = new ComponentSet ();
}
$result -> setComponentInfo ( " many-to-many " , $this , $parentClass , $table , $componentClass );
2008-02-25 03:10:37 +01:00
// If this record isn't in the database, then we want to hold onto this specific ComponentSet,
// because it's the only copy of the data that we have.
if ( ! $this -> isInDB ()) {
$this -> setComponent ( $componentName . '_' . $sum , $result );
}
2007-09-15 01:10:25 +02: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 $many_many Component .
* Use { @ link DataObjectSet -> setComponentInfo ()} to attach metadata to the
* resultset you ' re building with this query .
* Use { @ link DataObject -> buildDataObjectSet ()} to build a set out of the { @ link SQLQuery }
* object , and pass " ComponentSet " as a $containerClass .
*
* @ param string $componentName
* @ param string $filter
* @ param string | array $sort
* @ param string $join
* @ param string | array $limit
* @ return SQLQuery
*/
public function getManyManyComponentsQuery ( $componentName , $filter = " " , $sort = " " , $join = " " , $limit = " " ) {
list ( $parentClass , $componentClass , $parentField , $componentField , $table ) = $this -> many_many ( $componentName );
2008-08-11 01:17:51 +02:00
2008-08-09 07:57:44 +02:00
$componentObj = singleton ( $componentClass );
// Join expression is done on SiteTree.ID even if we link to Page; it helps work around
// database inconsistencies
$componentBaseClass = ClassInfo :: baseDataClass ( $componentClass );
$query = $componentObj -> extendedSQL (
" ` $table `. $parentField = $this->ID " , // filter
$sort ,
$limit ,
" INNER JOIN ` $table ` ON ` $table `. $componentField = ` $componentBaseClass `.ID " // join
);
array_unshift ( $query -> select , " ` $table `.* " );
2008-09-12 06:49:15 +02:00
// FIXME: We were having database crashing troubles with GIS content being accessed from with the link
// tracking join. In order to fix it, we're altering the query just for this many-many relation.
// The more long-term fix to this is to let developers specify which data columns they are actually interested
// in, and thereby optimise the query in a more loosely coupled fashion.
if ( $table == " SiteTree_LinkTracking " ) {
$filteredSelect = array ();
foreach ( $query -> select as $item ) {
if ( strpos ( $item , 'SiteTree' ) !== false ) $filteredSelect [] = $item ;
}
$query -> select = $filteredSelect ;
$query -> from = array (
" SiteTree " => $query -> from [ " SiteTree " ],
$query -> from [ 0 ],
);
}
2008-08-09 07:57:44 +02:00
if ( $filter ) $query -> where [] = $filter ;
if ( $join ) $query -> from [] = $join ;
2008-08-11 01:17:51 +02:00
2008-08-09 07:57:44 +02:00
return $query ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Creates an empty component for the given one - one or one - many relationship
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $componentName
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return DataObject The empty component . The exact class will be that of the components class .
*/
protected function createComponent ( $componentName ) {
if (( $componentClass = $this -> has_one ( $componentName )) || ( $componentClass = $this -> has_many ( $componentName ))) {
$component = new $componentClass ( null );
return $component ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
} else {
user_error ( " DataObject::createComponent(): Unknown 1-to-1 or 1-to-many component ' $componentName ' on class ' $this->class ' " , E_USER_ERROR );
}
}
2007-09-15 01:10:25 +02:00
2008-08-09 07:57:44 +02:00
/**
* Pull out a join clause for a many - many relationship .
*
* @ param string $componentName The many_many or belongs_many_many relation to join to .
* @ param string $baseTable The classtable that will already be included in the SQL query to which this join will be added .
* @ return string SQL join clause
*/
function getManyManyJoin ( $componentName , $baseTable ) {
if ( ! $componentClass = $this -> many_many ( $componentName )) {
user_error ( " DataObject::getComponents(): Unknown many-to-many component ' $componentName ' on class ' $this->class ' " , E_USER_ERROR );
}
$classes = array_reverse ( ClassInfo :: ancestry ( $this -> class ));
list ( $parentClass , $componentClass , $parentField , $componentField , $table ) = $this -> many_many ( $componentName );
2008-08-11 01:17:51 +02:00
2008-10-03 18:11:46 +02:00
$baseComponentClass = ClassInfo :: baseDataClass ( $componentClass );
2008-08-09 07:57:44 +02:00
if ( $baseTable == $parentClass ) {
2008-08-18 02:49:44 +02:00
return " LEFT JOIN ` $table ` ON (` $table `.` $parentField ` = ` $parentClass `.`ID` AND ` $table `.` $componentField ` = ' { $this -> ID } ') " ;
2008-08-09 07:57:44 +02:00
} else {
2008-10-03 18:11:46 +02:00
return " LEFT JOIN ` $table ` ON (` $table `.` $componentField ` = ` $baseComponentClass `.`ID` AND ` $table `.` $parentField ` = ' { $this -> ID } ') " ;
2008-08-09 07:57:44 +02:00
}
}
2008-08-11 01:17:51 +02:00
2008-08-09 08:18:32 +02:00
function getManyManyFilter ( $componentName , $baseTable ) {
list ( $parentClass , $componentClass , $parentField , $componentField , $table ) = $this -> many_many ( $componentName );
2008-08-11 01:17:51 +02:00
2008-08-09 08:18:32 +02:00
return " ` $table `.` $parentField ` = ' { $this -> ID } ' " ;
}
2008-08-09 07:57:44 +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 ) {
$candidate = eval ( " return isset( { $class } :: \$ has_one[ \$ component]) ? { $class } :: \$ has_one[ \$ component] : null; " );
if ( $candidate ) {
return $candidate ;
}
} else {
eval ( " \$ items = isset( \$ items) ? array_merge((array) { $class } :: \$ has_one, (array) \$ items) : (array) { $class } :: \$ has_one; " );
}
}
return isset ( $items ) ? $items : null ;
}
2007-09-15 01:10:25 +02:00
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
*
2007-07-19 12:40:28 +02:00
* @ return array The database fields
*/
2008-08-09 04:16:46 +02:00
public function db ( $component = null ) {
2007-07-19 12:40:28 +02:00
$classes = ClassInfo :: ancestry ( $this );
$good = false ;
$items = array ();
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
if ( ! $good ) {
if ( $class == 'DataObject' ) {
$good = true ;
}
continue ;
}
2008-08-09 04:16:46 +02:00
if ( $component ) {
$candidate = eval ( " return isset( { $class } :: \$ db[ \$ component]) ? { $class } :: \$ db[ \$ component] : null; " );
if ( $candidate ) {
return $candidate ;
}
} else {
2008-08-09 05:54:55 +02:00
eval ( " \$ items = array_merge((array) \$ items, (array) { $class } :: \$ db); " );
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
/**
* Return the class of a one - to - many component . If $component is null , return all of the one - to - many 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 - many component , or an array of all one - to - many components and their classes .
*/
public function has_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 ) {
2008-04-26 08:53:23 +02:00
if ( in_array ( $class , array ( 'ViewableData' , 'Object' , 'DataObject' ))) continue ;
2007-07-19 12:40:28 +02:00
if ( $component ) {
$candidate = eval ( " return isset( { $class } :: \$ has_many[ \$ component]) ? { $class } :: \$ has_many[ \$ component] : null; " );
2007-09-15 01:31:08 +02:00
$candidate = eval ( " if ( isset( { $class } :: \$ has_many[ \$ component]) ) { return { $class } :: \$ has_many[ \$ component]; } else { return false; } " );
2007-07-19 12:40:28 +02:00
if ( $candidate ) {
return $candidate ;
}
} else {
eval ( " \$ items = isset( \$ items) ? array_merge((array) { $class } :: \$ has_many, (array) \$ items) : (array) { $class } :: \$ has_many; " );
}
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
return isset ( $items ) ? $items : null ;
}
/**
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 ) {
2008-07-18 00:06:34 +02:00
$manyMany = singleton ( $class ) -> stat ( 'many_many' );
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
2008-07-18 00:06:34 +02:00
$belongsManyMany = singleton ( $class ) -> stat ( 'belongs_many_many' );
$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
2008-07-18 00:06:34 +02:00
$otherManyMany = singleton ( $candidate ) -> stat ( 'many_many' );
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 {
eval ( " \$ items = isset( \$ items) ? array_merge((array) { $class } :: \$ many_many, (array) \$ items) : (array) { $class } :: \$ many_many; " );
eval ( " \$ items = array_merge((array) { $class } :: \$ belongs_many_many, (array) \$ items); " );
}
}
return isset ( $items ) ? $items : null ;
}
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
* @ usedby { @ link ModelAdmin }
* @ 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
*
* @ usedby { @ 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
2008-08-06 04:43:46 +02:00
* @ return FieldSet
*/
2008-10-14 00:20:41 +02:00
public function scaffoldSearchFields ( $_params = null ) {
$params = array_merge (
array (
'fieldClasses' => false ,
'restrictFields' => false
),
( array ) $_params
);
2008-08-06 04:43:46 +02:00
$fields = new FieldSet ();
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 } .
2008-08-06 04:43:46 +02:00
* @ return FieldSet
*/
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
2008-10-14 00:20:41 +02:00
return $fs -> getFieldSet ();
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 ,
* which returns a { @ link FieldSet } suitable for a { @ link Form } object .
* 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
* or decorate onto it by using { @ link DataObjectDecorator -> updateCMSFields ()} .
2008-08-09 07:04:15 +02:00
*
2008-08-06 04:43:46 +02:00
* < example >
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
*
2008-08-06 04:43:46 +02:00
* public function getCMSFields () {
* $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 ;
* }
* }
* </ example >
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
*
2008-10-14 00:20:41 +02:00
* @ param array $params See { @ link scaffoldFormFields ()}
2008-10-03 18:21:09 +02:00
* @ return FieldSet Returns a TabSet for usage within the CMS - don ' t use for frontend forms .
2008-08-06 04:43:46 +02:00
*/
2008-10-14 00:20:41 +02:00
public function getCMSFields ( $params = null ) {
$tabbedFields = $this -> scaffoldFormFields ( array_merge (
array (
'includeRelations' => true ,
'tabbed' => true ,
'ajaxSafe' => true
),
( array ) $params
));
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
}
/**
* 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
* subclass , or decorate it by { @ link DataObjectDecorator -> updateFormFields ()} .
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 ()}
2008-10-03 18:21:09 +02:00
* @ return FieldSet Always returns a simple field collection without TabSet .
*/
2008-10-14 00:20:41 +02:00
public function getFrontEndFields ( $params = null ) {
$untabbedFields = $this -> scaffoldFormFields ( $params );
$this -> extend ( 'updateFormFields' , $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
/**
* Checks if the given fields have been filled out .
* Pass this method a number of field names , it will return true if they all have values .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param array | string $args , ... The field names may be passed either as an array , or as multiple parameters .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return boolean True if all fields have values , otherwise false
*/
public function filledOut ( $args ) {
// Field names can be passed as arguments or an array
if ( ! is_array ( $args )) $args = func_get_args ();
foreach ( $args as $arg ) {
if ( ! $this -> $arg ) {
return false ;
}
}
2007-09-15 01:10:25 +02:00
return true ;
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
*/
protected 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 ];
2008-08-09 07:04:15 +02:00
2008-08-09 04:16:46 +02:00
// Otherwise, we need to determine if this is a complex field
$fieldClass = $this -> db ( $field );
if ( $fieldClass && ClassInfo :: classImplements ( $fieldClass , 'CompositeDBField' )) {
$helperPair = $this -> castingHelperPair ( $field );
$constructor = $helperPair [ 'castingHelper' ];
$fieldName = $field ;
$fieldObj = eval ( $constructor );
if ( isset ( $this -> record [ $field ])) $fieldObj -> setValue ( $this -> record [ $field ], $this -> record );
$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
/**
* Return a map of all the fields for this record .
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 () {
return $this -> record ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Return the fields that have changed .
* The change level affects what the functions defines as " changed " :
* Level 1 will return strict changes , even !== ones .
* Level 2 is more lenient , it will onlr return real data changes , for example a change from 0 to null
* 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 ();
2007-07-19 12:40:28 +02:00
if ( $databaseFieldsOnly ) {
$customDatabaseFields = $this -> customDatabaseFields ();
$fields = array_intersect_key ( $this -> changed , $customDatabaseFields );
} 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 ) {
foreach ( $fields as $name => $level ) {
if ( $level < $changeLevel ) {
unset ( $fields [ $name ]);
}
}
}
2008-08-11 05:13:53 +02:00
2008-09-12 06:49:15 +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
}
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
*/
function setField ( $fieldName , $val ) {
2008-08-09 04:16:46 +02:00
// Situation 1: Passing a DBField
if ( $val instanceof DBField ) {
$val -> Name = $fieldName ;
2007-07-19 12:40:28 +02:00
$this -> record [ $fieldName ] = $val ;
2008-08-11 01:17:51 +02:00
2008-08-09 07:04:15 +02:00
// Situation 2: Passing a literal
2008-08-09 04:16:46 +02:00
} else {
$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 ;
2008-10-08 04:25:26 +02:00
if ( ! 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 ;
}
// value is always saved back when strict check succeeds
$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 ) {
$fieldObj = eval ( $castingHelper );
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 ) {
2008-10-09 16:38:46 +02:00
return array_key_exists ( $field , $this -> record ) || $this -> hasDatabaseField ( $field ) || $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 ) {
return array_key_exists ( $field , $this -> databaseFields ());
}
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
*/
public function hasOwnTableDatabaseField ( $field ) {
// 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 " ;
2008-10-16 10:59:40 +02:00
if ( $field == " LastEdited " && get_parent_class ( $this ) == " DataObject " ) return " SSDatetime " ;
if ( $field == " Created " && get_parent_class ( $this ) == " DataObject " ) return " SSDatetime " ;
2008-09-30 02:20:30 +02:00
// Add fields from Versioned decorator
if ( $field == " Version " ) return $this -> hasExtension ( 'Versioned' ) ? " Int " : false ;
// get cached fieldmap
$fieldMap = $this -> uninherited ( '_cache_hasOwnTableDatabaseField' );
// if no fieldmap is cached, get all fields
if ( ! $fieldMap ) {
// all $db fields on this specific class (no parents)
$fieldMap = $this -> uninherited ( 'db' , true );
// all has_one relations on this specific class,
// add foreign key
$hasOne = $this -> uninherited ( 'has_one' , true );
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
}
// set cached fieldmap
$this -> set_uninherited ( '_cache_hasOwnTableDatabaseField' , $fieldMap );
}
// Remove string-based "constructor-arguments" from the DBField definition
return isset ( $fieldMap [ $field ]) ? strtok ( $fieldMap [ $field ], '(' ) : null ;
}
2008-03-12 10:21:49 +01:00
2007-07-19 12:40:28 +02:00
/**
* Returns true if the member is allowed to do the given action .
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
*/
function can ( $perm , $member = null ) {
if ( ! isset ( $member )) {
$member = Member :: currentUser ();
}
if ( $member && $member -> isAdmin ()) {
return true ;
}
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
2007-07-19 12:40:28 +02:00
$query = new SQLQuery (
" `Page_Can $perm `.PageID " ,
2008-08-09 07:04:15 +02:00
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 " ) {
$query = new SQLQuery ( " `SiteTree`.ID " , array (
" `SiteTree` " ,
" LEFT JOIN `Page_CanView` ON `Page_CanView`.PageID = `SiteTree`.ID "
), " `Page_CanView`.PageID IS NULL " );
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
}
$this -> set_uninherited ( 'permissionCache' , $permissionCache );
}
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
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 ) {
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
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 () {
$val = " <h3>Database record: $this->class </h3><ul> " ;
if ( $this -> record ) foreach ( $this -> record as $fieldName => $fieldVal ) {
$val .= " <li style= \" list-style-type: disc; margin-left: 20px \" > $fieldName : " . Debug :: text ( $fieldVal ) . " </li> " ;
}
$val .= " </ul> " ;
return $val ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
2008-09-30 02:20:30 +02:00
* @ deprecated 2.3 ( For external use ) Please use hasField (), hasDatabaseField (), hasOwnTableDatabaseField () instead
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $field Name of the field
* @ return string The field type of the given field
2007-09-15 01:10:25 +02:00
*/
2008-08-09 07:04:15 +02:00
public function fieldExists ( $field ) {
2008-09-30 02:20:30 +02:00
return $this -> hasOwnTableDatabaseField ( $field );
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 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
} else if ( $helperPair = $this -> castingHelperPair ( $fieldName )) {
$constructor = $helperPair [ 'castingHelper' ];
$obj = eval ( $constructor );
2008-08-09 04:16:46 +02:00
$obj -> setValue ( $this -> $fieldName , $this -> record );
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 ))) {
2008-10-14 00:20:41 +02:00
$val = ( isset ( $this -> record [ $fieldName ])) ? $this -> record [ $fieldName ] : null ;
return DBField :: create ( 'ForeignKey' , $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 ) {
$parts = explode ( '.' , $fieldPath );
$fieldName = array_pop ( $parts );
$component = $this ;
foreach ( $parts as $relation ) {
if ( $rel = $component -> has_one ( $relation )) {
2008-08-09 07:04:15 +02:00
$component = singleton ( $rel );
2008-08-09 06:53:34 +02:00
} elseif ( $rel = $component -> has_many ( $relation )) {
$component = singleton ( $rel );
2008-08-09 07:04:15 +02:00
} elseif ( $rel = $component -> many_many ( $relation )) {
$component = singleton ( $rel [ 1 ]);
2008-08-09 06:53:34 +02:00
}
}
2008-08-11 01:17:51 +02:00
2008-08-09 07:04:15 +02:00
$object = $component -> dbObject ( $fieldName );
2008-08-11 05:03:52 +02:00
if ( ! ( $object instanceof DBField ) && ! ( $object instanceof ComponentSet )) {
2008-08-11 01:29:30 +02:00
user_error ( " Unable to traverse to related object field [ $fieldPath ] on [ $this->class ] " , E_USER_ERROR );
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
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
/**
* Build a { @ link SQLQuery } object to perform the given query .
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 .
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 .
* @ param string | array $limit A limit expression to be inserted into the LIMIT clause .
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 .
* @ param boolean $restictClasses Restrict results to only objects of either this class of a subclass of this class
* @ param string $having A filter to be inserted into the HAVING clause .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return SQLQuery Query built .
*/
public function buildSQL ( $filter = " " , $sort = " " , $limit = " " , $join = " " , $restrictClasses = true , $having = " " ) {
// Find a default sort
if ( ! $sort ) {
$sort = $this -> stat ( 'default_sort' );
}
// Get the tables to join to
$tableClasses = ClassInfo :: dataClassesFor ( $this -> class );
if ( ! $tableClasses ) {
user_error ( " DataObject::buildSQL: Can't find data classes (classes linked to tables) for $this->class " , E_USER_ERROR );
}
$baseClass = array_shift ( $tableClasses );
2008-02-25 03:10:37 +01:00
$select = array ( " ` $baseClass `.* " );
2008-08-09 07:04:15 +02:00
2007-07-19 12:40:28 +02:00
// Build our intial query
2008-08-09 07:57:44 +02:00
$query = new SQLQuery ( $select );
$query -> from ( " ` $baseClass ` " );
$query -> where ( $filter );
$query -> orderby ( $sort );
$query -> limit ( $limit );
2008-08-11 01:17:51 +02:00
2008-08-09 04:16:46 +02:00
// Add SQL for multi-value fields on the base table
$databaseFields = $this -> databaseFields ();
if ( $databaseFields ) foreach ( $databaseFields as $k => $v ) {
if ( ! in_array ( $k , array ( 'ClassName' , 'LastEdited' , 'Created' ))) {
if ( ClassInfo :: classImplements ( $v , 'CompositeDBField' )) {
2008-10-01 02:55:25 +02:00
$this -> dbObject ( $k ) -> addToQuery ( $query );
2008-08-09 04:16:46 +02:00
}
}
}
2007-07-19 12:40:28 +02:00
// Join all the tables
2008-08-12 04:51:33 +02:00
if ( $tableClasses && self :: $subclass_access ) {
2007-07-19 12:40:28 +02:00
foreach ( $tableClasses as $tableClass ) {
$query -> from [ $tableClass ] = " LEFT JOIN ` $tableClass ` ON ` $tableClass `.ID = ` $baseClass `.ID " ;
$query -> select [] = " ` $tableClass `.* " ;
2008-08-09 04:16:46 +02:00
// Add SQL for multi-value fields
$SNG = singleton ( $tableClass );
2008-08-09 08:46:48 +02:00
$databaseFields = $SNG -> databaseFields ();
if ( $databaseFields ) foreach ( $databaseFields as $k => $v ) {
2008-08-09 04:16:46 +02:00
if ( ! in_array ( $k , array ( 'ClassName' , 'LastEdited' , 'Created' ))) {
if ( ClassInfo :: classImplements ( $v , 'CompositeDBField' )) {
2008-10-01 02:55:25 +02:00
$SNG -> dbObject ( $k ) -> addToQuery ( $query );
2008-08-09 04:16:46 +02:00
}
}
2008-08-09 04:00:40 +02:00
}
2007-07-19 12:40:28 +02:00
}
}
2008-08-09 04:16:46 +02:00
2007-07-19 12:40:28 +02:00
$query -> select [] = " ` $baseClass `.ID " ;
$query -> select [] = " if(` $baseClass `.ClassName,` $baseClass `.ClassName,' $baseClass ') AS RecordClassName " ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
// Get the ClassName values to filter to
$classNames = ClassInfo :: subclassesFor ( $this -> class );
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
if ( ! $classNames ) {
user_error ( " DataObject::get() Can't find data sub-classes for ' $callerClass ' " );
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
// If querying the base class, don't bother filtering on class name
if ( $restrictClasses && $this -> class != $baseClass ) {
// Get the ClassName values to filter to
$classNames = ClassInfo :: subclassesFor ( $this -> class );
if ( ! $classNames ) {
user_error ( " DataObject::get() Can't find data sub-classes for ' $callerClass ' " );
}
$query -> where [] = " ` $baseClass `.ClassName IN (' " . implode ( " ',' " , $classNames ) . " ') " ;
}
if ( $having ) {
$query -> having [] = $having ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
if ( $join ) {
$query -> from [] = $join ;
$query -> groupby [] = reset ( $query -> from ) . " .ID " ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
return $query ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
2007-09-16 18:19:31 +02:00
* Like { @ link buildSQL }, but applies the extension modifications .
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 .
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 .
* @ param string | array $limit A limit expression to be inserted into the LIMIT clause .
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 .
* @ param string $having A filter to be inserted into the HAVING clause .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return SQLQuery Query built
*/
public function extendedSQL ( $filter = " " , $sort = " " , $limit = " " , $join = " " , $having = " " ){
$query = $this -> buildSQL ( $filter , $sort , $limit , $join , true , $having );
$this -> extend ( 'augmentSQL' , $query );
2007-09-15 01:10:25 +02:00
return $query ;
2007-07-19 12:40:28 +02:00
}
/**
2008-02-25 03:10:37 +01:00
* Get a bunch of fields in an HTML LI , like this :
* - name : value
* - name : value
* - name : value
2007-09-15 01:10:25 +02:00
*
2008-09-30 02:20:30 +02:00
* @ deprecated 2.3 Use custom code
2007-07-19 12:40:28 +02:00
* @ return string The fields as an HTML unordered list
*/
function listOfFields () {
$fields = func_get_args ();
$result = " <ul> \n " ;
foreach ( $fields as $field )
2008-08-09 07:04:15 +02:00
$result .= " <li><b> $field :</b> " . $this -> $field . " </li> \n " ;
2007-07-19 12:40:28 +02:00
$result .= " </ul> " ;
return $result ;
}
/**
* 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
*/
2008-02-25 03:10:37 +01:00
public static function get ( $callerClass , $filter = " " , $sort = " " , $join = " " , $limit = " " , $containerClass = " DataObjectSet " ) {
return singleton ( $callerClass ) -> instance_get ( $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
/**
* The internal function that actually performs the querying for get () .
* DataObject :: get ( " Table " , " filter " ) is the same as singleton ( " Table " ) -> instance_get ( " filter " )
*
* @ 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 " ) {
$query = $this -> extendedSQL ( $filter , $sort , $limit , $join );
2007-07-19 12:40:28 +02:00
$records = $query -> execute ();
2008-08-09 07:04:15 +02:00
2007-07-19 12:40:28 +02:00
$ret = $this -> buildDataObjectSet ( $records , $containerClass , $query , $this -> class );
if ( $ret ) $ret -> parseQueryLimit ( $query );
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
return $ret ;
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Take a database { @ link Query } and instanciate an object for each record .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param Query | array $records The database records , a { @ link Query } object or an array of maps .
* @ 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
*/
function buildDataObjectSet ( $records , $containerClass = " DataObjectSet " , $query = null , $baseClass = null ) {
foreach ( $records as $record ) {
if ( ! $record [ 'RecordClassName' ]) {
$record [ 'RecordClassName' ] = $record [ 'ClassName' ];
}
if ( class_exists ( $record [ 'RecordClassName' ])) {
$results [] = new $record [ 'RecordClassName' ]( $record );
} else {
$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 );
}
}
2008-08-09 07:04:15 +02:00
/**
* A cache used by get_one .
* @ var array
*/
protected static $cache_get_one ;
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* 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 = " " ) {
2008-02-25 03:10:37 +01:00
$sum = md5 ( " { $filter } _ { $orderby } " );
2008-08-11 07:31:13 +02:00
// Flush destroyed items out of the cache
if ( $cache && isset ( DataObject :: $cache_get_one [ $callerClass ][ $sum ]) && DataObject :: $cache_get_one [ $callerClass ][ $sum ] instanceof DataObject && DataObject :: $cache_get_one [ $callerClass ][ $sum ] -> destroyed ) {
DataObject :: $cache_get_one [ $callerClass ][ $sum ] = false ;
}
if ( ! $cache || ! isset ( DataObject :: $cache_get_one [ $callerClass ][ $sum ])) {
2007-07-19 12:40:28 +02:00
$item = singleton ( $callerClass ) -> instance_get_one ( $filter , $orderby );
if ( $cache ) {
2008-02-25 03:10:37 +01:00
DataObject :: $cache_get_one [ $callerClass ][ $sum ] = $item ;
if ( ! DataObject :: $cache_get_one [ $callerClass ][ $sum ]) {
DataObject :: $cache_get_one [ $callerClass ][ $sum ] = false ;
2007-07-19 12:40:28 +02:00
}
}
}
2008-02-25 03:10:37 +01:00
return $cache ? DataObject :: $cache_get_one [ $callerClass ][ $sum ] : $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 )
2007-07-19 12:40:28 +02:00
*/
public function flushCache () {
2007-08-16 08:32:49 +02:00
if ( $this -> class == 'DataObject' ) {
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
2008-08-11 07:31:13 +02:00
if ( ! self :: $cache_get_one ) return ;
2007-07-19 12:40:28 +02:00
$classes = ClassInfo :: ancestry ( $this -> class );
foreach ( $classes as $class ) {
2008-08-11 07:31:13 +02:00
if ( isset ( self :: $cache_get_one [ $class ])) unset ( self :: $cache_get_one [ $class ]);
2007-07-19 12:40:28 +02:00
}
2008-08-11 05:03:52 +02:00
$this -> componentCache = array ();
2007-07-19 12:40:28 +02:00
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Does the hard work for get_one ()
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 .
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 function instance_get_one ( $filter , $orderby = null ) {
$query = $this -> buildSQL ( $filter );
$query -> limit = " 1 " ;
if ( $orderby ) {
$query -> orderby = $orderby ;
}
$this -> extend ( 'augmentSQL' , $query );
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
$records = $query -> execute ();
$records -> rewind ();
$record = $records -> current ();
if ( $record ) {
// Mid-upgrade, the database can have invalid RecordClassName values that need to be guarded against.
if ( class_exists ( $record [ 'RecordClassName' ])) {
$record = new $record [ 'RecordClassName' ]( $record );
} else {
$record = new $this -> class ( $record );
2007-09-16 18:40:57 +02:00
}
2007-07-19 12:40:28 +02:00
// Rather than restrict classes at the SQL-query level, we now check once the object has been instantiated
// This lets us check up on weird errors where the class has been incorrectly set, and give warnings to our
// developers
return $record ;
}
}
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
/**
* Return the SiteTree object with the given URL segment .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $urlSegment The URL segment , eg 'home'
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return SiteTree The object with the given URL segment
*/
public static function get_by_url ( $urlSegment ) {
return DataObject :: get_one ( " SiteTree " , " URLSegment = ' " . addslashes (( string ) $urlSegment ) . " ' " );
}
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
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return DataObject The element
*/
public static function get_by_id ( $callerClass , $id ) {
if ( is_numeric ( $id )) {
2008-02-25 03:10:37 +01:00
if ( singleton ( $callerClass ) instanceof DataObject ) {
$tableClasses = ClassInfo :: dataClassesFor ( $callerClass );
$baseClass = array_shift ( $tableClasses );
return DataObject :: get_one ( $callerClass , " ` $baseClass `.`ID` = $id " );
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 {
return DataObject :: get_one ( $callerClass , " `ID` = $id " );
}
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 .
*/
public function requireTable () {
2007-09-15 01:10:25 +02:00
// Only build the table if we've actually got fields
2007-07-19 12:40:28 +02:00
$fields = $this -> databaseFields ();
2008-08-09 07:04:15 +02:00
$indexes = $this -> databaseIndexes ();
2007-07-19 12:40:28 +02:00
if ( $fields ) {
DB :: requireTable ( $this -> class , $fields , $indexes );
} 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 (
" { $this -> class } ID " => true ,
2008-08-09 07:04:15 +02:00
(( $this -> class == $childClass ) ? " ChildID " : " { $childClass } ID " ) => true ,
2007-07-19 12:40:28 +02:00
);
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
DB :: requireTable ( " { $this -> class } _ $relationship " , $manymanyFields , $manymanyIndexes );
}
}
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 () .
*/
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 ) {
$obj = new $className ( $record );
$obj -> write ();
2007-07-19 12:40:28 +02:00
}
2008-08-11 02:14:48 +02:00
Database :: alteration_message ( " Added default records to $className table " , " created " );
2007-07-19 12:40:28 +02:00
}
}
2007-12-18 22:24:23 +01:00
// Let any extentions make their own database default data
$this -> extend ( 'augmentDefaultRecords' , $dummy );
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 complete set of database fields , including Created , LastEdited and ClassName .
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
* @ return array A map of field name to class of all databases fields on this object
2007-09-15 01:10:25 +02:00
*
2007-07-19 12:40:28 +02:00
*/
public function databaseFields () {
// For base tables, add a classname field
if ( $this -> parentClass () == 'DataObject' ) {
$childClasses = ClassInfo :: subclassesFor ( $this -> class );
return array_merge (
2008-08-09 07:04:15 +02:00
array (
2007-07-19 12:40:28 +02:00
" ClassName " => " Enum(' " . implode ( " , " , $childClasses ) . " ') " ,
2008-10-16 10:59:40 +02:00
" Created " => " SSDatetime " ,
" LastEdited " => " SSDatetime " ,
2008-08-09 07:04:15 +02:00
),
( array ) $this -> customDatabaseFields ()
2007-07-19 12:40:28 +02:00
);
2007-09-15 01:10:25 +02:00
2008-08-09 07:04:15 +02:00
// Child table
2007-07-19 12:40:28 +02:00
} else {
2008-10-09 16:39:24 +02:00
return ( array ) $this -> customDatabaseFields ();
2007-07-19 12:40:28 +02:00
}
}
/**
2008-09-30 02:20:30 +02:00
* Get the custom database fields for this object , from self :: $db and self :: $has_one ,
* but not built - in fields like ID , ClassName , Created , LastEdited .
*
* @ return array
2007-07-19 12:40:28 +02:00
*/
public function customDatabaseFields () {
$db = $this -> uninherited ( 'db' , true );
$has_one = $this -> uninherited ( 'has_one' , true );
2007-09-15 01:10:25 +02:00
2007-07-19 12:40:28 +02:00
$def = $db ;
if ( $has_one ) {
foreach ( $has_one as $field => $joinTo ) {
2008-10-14 00:20:41 +02:00
$def [ $field . 'ID' ] = " ForeignKey " ;
2007-07-19 12:40:28 +02:00
}
}
2007-09-15 01:10:25 +02:00
2008-10-09 16:39:24 +02:00
return ( array ) $def ;
2007-07-19 12:40:28 +02:00
}
2008-08-09 07:04:15 +02: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 () {
$fields = array ();
$currentObj = $this ;
while ( get_class ( $currentObj ) != 'DataObject' ) {
2008-08-09 07:57:44 +02:00
$fields = array_merge ( $fields , ( array ) $currentObj -> customDatabaseFields ());
2008-08-06 05:43:48 +02:00
$currentObj = singleton ( $currentObj -> parentClass ());
}
2008-10-09 16:39:24 +02: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
// augumenting fields, so decorators can apply consistent checks
// but also after augumenting fields, because the decorator
// 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
2008-09-30 02:20:30 +02:00
// apply DataObjectDecorators 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 ()
*
2008-08-09 08:53:26 +02:00
* @ return array of all element labels if no argument given
* @ return string of label if field
*/
2008-10-03 20:38:52 +02:00
public function fieldLabels () {
2008-08-11 01:29:30 +02:00
$customLabels = $this -> stat ( 'field_labels' );
$autoLabels = array ();
2008-11-01 14:23:38 +01:00
// 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 ) singleton ( $ancestorClass ) -> uninherited ( 'db' , true ),
'has_one' => ( array ) singleton ( $ancestorClass ) -> uninherited ( 'has_one' , true ),
'has_many' => ( array ) singleton ( $ancestorClass ) -> uninherited ( 'has_many' , true ),
'many_many' => ( array ) singleton ( $ancestorClass ) -> uninherited ( 'many_many' , true )
);
foreach ( $types as $type => $attrs ) {
foreach ( $attrs as $name => $spec )
$autoLabels [ $name ] = _t ( " { $ancestorClass } . { $type } _ { $name } " , FormField :: name_to_label ( $name ));
}
}
2008-10-28 04:03:16 +01:00
$labels = array_merge (( array ) $autoLabels , ( array ) $customLabels );
$this -> extend ( 'updateFieldLabels' , $labels );
return $labels ;
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' ;
if ( $this -> hasField ( 'Title' )) $fields [ 'Title' ] = 'Title' ;
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' ;
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 ();
2008-08-11 01:29:30 +02:00
foreach ( $this -> searchableFields () as $name => $spec ) {
$filterClass = $spec [ 'filter' ];
2008-08-11 05:03:52 +02:00
// if $filterClass is not set a name of any subclass of SearchFilter than assing 'PartiailMatchFilter' to it
if ( ! is_subclass_of ( $filterClass , 'SearchFilter' )) {
$filterClass = 'PartialMatchFilter' ;
}
2008-08-11 01:29:30 +02:00
$filters [ $name ] = new $filterClass ( $name );
2008-08-06 05:43:48 +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 ;
}
/**
* Sets a 'context object' that can be used to provide hints about how to process a particular get / get_one request .
2007-08-16 08:32:49 +02:00
* In particular , DataObjectDecorators can use this to amend queries more effectively .
* Care must be taken to unset the context object after you ' re done with it , otherwise you will have a stale context ,
* which could cause horrible bugs .
*/
public static function set_context_obj ( $obj ) {
2008-10-30 22:51:59 +01:00
if ( $obj && self :: $context_obj ) user_error ( " Dataobject::set_context_obj passed " . $obj -> class . " . " . $obj -> ID . " when there is already a context: " . self :: $context_obj -> class . '.' . self :: $context_obj -> ID , E_USER_WARNING );
2007-08-16 08:32:49 +02:00
self :: $context_obj = $obj ;
}
2008-08-09 07:04:15 +02:00
2007-08-16 08:32:49 +02:00
/**
* Retrieve the current context object .
*/
public static function context_obj () {
return self :: $context_obj ;
}
2007-09-16 18:40:57 +02:00
2008-08-09 06:06:52 +02:00
/**
* @ ignore
*/
2007-08-16 08:32:49 +02:00
protected static $context_obj = null ;
2008-08-09 07:04:15 +02:00
2008-08-12 04:51:33 +02:00
/*
* @ ignore
*/
private static $subclass_access = true ;
/**
* Temporarily disable subclass access in data object qeur
*/
static function disable_subclass_access () {
self :: $subclass_access = false ;
}
static function enable_subclass_access () {
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
*/
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 (
2008-10-16 10:59:40 +02:00
" LastEdited " => " SSDatetime " ,
" Created " => " SSDatetime " ,
2008-08-11 04:25:44 +02:00
" Title " => 'Text' ,
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 a field is in this array , then create a database index
* on that field . This is a map from fieldname to index type .
* @ 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 } .
* This is a map from classname 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
/**
* one - to - one relationship definitions .
* This is a map from component name to data type .
* @ var array
*/
public static $has_one = null ;
/**
* one - to - many relationship definitions .
* This is a map from component name to data type .
2008-08-09 07:04:15 +02:00
*
2008-02-25 03:10:37 +01:00
* Caution : Because this doesn ' t define any data structure itself , you should
2008-08-09 07:04:15 +02:00
* specify a $has_one relationship on the other end of the relationship .
2008-02-25 03:10:37 +01:00
* Also , if the $has_one relationship on the other end has multiple
* definitions of this class ( e . g . two different relationships to the Member
* object ), then you need to write a custom accessor ( e . g . overload the
* function from the key of this array ), because sapphire won ' t know which
* to access .
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 .
* @ 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
/**
* 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 ();
$db = $this -> uninherited ( 'db' , true );
if ( $db ) foreach ( $db as $name => $type ) {
$entities [ " { $this -> class } .db_ { $name } " ] = array (
$name ,
PR_MEDIUM ,
'Name of the object property, mainly used for automatically generating forms'
);
}
$has_many = $this -> uninherited ( 'has_many' , true );
if ( $has_many ) foreach ( $has_many as $name => $class ) {
$entities [ " { $this -> class } .has_many_ { $name } " ] = array (
$name ,
PR_MEDIUM ,
'Name of an object relation, mainly used for automatically generating forms'
);
}
$many_many = $this -> uninherited ( 'many_many' , true );
if ( $many_many ) foreach ( $many_many as $name => $class ) {
$entities [ " { $this -> class } .many_many_ { $name } " ] = array (
$name ,
PR_MEDIUM ,
'Name of an object relation, mainly used for automatically generating forms'
);
}
$entities [ " { $this -> class } .SINGULARNAME " ] = array (
2008-11-01 14:23:38 +01:00
$this -> singular_name (),
2008-10-29 22:07:17 +01:00
PR_MEDIUM ,
'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 (),
2008-10-29 22:07:17 +01:00
PR_MEDIUM ,
'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the interface'
);
return $entities ;
}
2008-08-09 07:04:15 +02:00
2007-07-19 12:40:28 +02:00
}
2007-09-16 18:40:57 +02:00
2008-08-11 07:31:13 +02:00
?>