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