· 10 years ago · Sep 15, 2016, 03:20 PM
1<?php
2/**
3 * A single database record & abstract class for the data-access-model.
4 *
5 * <h2>Extensions</h2>
6 *
7 * See {@link Extension} and {@link DataExtension}.
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 * function canView($member = false) {
19 * return Permission::check('ARTICLE_VIEW');
20 * }
21 * function canEdit($member = false) {
22 * return Permission::check('ARTICLE_EDIT');
23 * }
24 * function canDelete() {
25 * return Permission::check('ARTICLE_DELETE');
26 * }
27 * function canCreate() {
28 * return Permission::check('ARTICLE_CREATE');
29 * }
30 * 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 * function canView($member = false) {
47 * if(!$member) $member = Member::currentUser();
48 * return $member->inGroup('Subscribers');
49 * }
50 * 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 framework
67 * @subpackage model
68 *
69 * @property integer ID ID of the DataObject, 0 if the DataObject doesn't exist in database.
70 * @property string ClassName Class name of the DataObject
71 * @property string LastEdited Date and time of DataObject's last modification.
72 * @property string Created Date and time of DataObject creation.
73 */
74class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider {
75
76 /**
77 * Human-readable singular name.
78 * @var string
79 * @config
80 */
81 private static $singular_name = null;
82
83 /**
84 * Human-readable plural name
85 * @var string
86 * @config
87 */
88 private static $plural_name = null;
89
90 /**
91 * Allow API access to this object?
92 * @todo Define the options that can be set here
93 * @config
94 */
95 private static $api_access = false;
96
97 /**
98 * True if this DataObject has been destroyed.
99 * @var boolean
100 */
101 public $destroyed = false;
102
103 /**
104 * The DataModel from this this object comes
105 */
106 protected $model;
107
108 /**
109 * Data stored in this objects database record. An array indexed by fieldname.
110 *
111 * Use {@link toMap()} if you want an array representation
112 * of this object, as the $record array might contain lazy loaded field aliases.
113 *
114 * @var array
115 */
116 protected $record;
117
118 /**
119 * Represents a field that hasn't changed (before === after, thus before == after)
120 */
121 const CHANGE_NONE = 0;
122
123 /**
124 * Represents a field that has changed type, although not the loosely defined value.
125 * (before !== after && before == after)
126 * E.g. change 1 to true or "true" to true, but not true to 0.
127 * Value changes are by nature also considered strict changes.
128 */
129 const CHANGE_STRICT = 1;
130
131 /**
132 * Represents a field that has changed the loosely defined value
133 * (before != after, thus, before !== after))
134 * E.g. change false to true, but not false to 0
135 */
136 const CHANGE_VALUE = 2;
137
138 /**
139 * An array indexed by fieldname, true if the field has been changed.
140 * Use {@link getChangedFields()} and {@link isChanged()} to inspect
141 * the changed state.
142 *
143 * @var array
144 */
145 private $changed;
146
147 /**
148 * The database record (in the same format as $record), before
149 * any changes.
150 * @var array
151 */
152 protected $original;
153
154 /**
155 * Used by onBeforeDelete() to ensure child classes call parent::onBeforeDelete()
156 * @var boolean
157 */
158 protected $brokenOnDelete = false;
159
160 /**
161 * Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite()
162 * @var boolean
163 */
164 protected $brokenOnWrite = false;
165
166 /**
167 * @config
168 * @var boolean Should dataobjects be validated before they are written?
169 * Caution: Validation can contain safeguards against invalid/malicious data,
170 * and check permission levels (e.g. on {@link Group}). Therefore it is recommended
171 * to only disable validation for very specific use cases.
172 */
173 private static $validation_enabled = true;
174
175 /**
176 * Static caches used by relevant functions.
177 */
178 public static $cache_has_own_table = array();
179 protected static $_cache_db = array();
180 protected static $_cache_get_one;
181 protected static $_cache_get_class_ancestry;
182 protected static $_cache_composite_fields = array();
183 protected static $_cache_is_composite_field = array();
184 protected static $_cache_custom_database_fields = array();
185 protected static $_cache_field_labels = array();
186
187 // base fields which are not defined in static $db
188 private static $fixed_fields = array(
189 'ID' => 'Int',
190 'ClassName' => 'Enum',
191 'LastEdited' => 'SS_Datetime',
192 'Created' => 'SS_Datetime',
193 );
194
195 /**
196 * Non-static relationship cache, indexed by component name.
197 */
198 protected $components;
199
200 /**
201 * Non-static cache of has_many and many_many relations that can't be written until this object is saved.
202 */
203 protected $unsavedRelations;
204
205 /**
206 * Returns when validation on DataObjects is enabled.
207 *
208 * @deprecated 3.2 Use the "DataObject.validation_enabled" config setting instead
209 * @return bool
210 */
211 public static function get_validation_enabled() {
212 Deprecation::notice('3.2', 'Use the "DataObject.validation_enabled" config setting instead');
213 return Config::inst()->get('DataObject', 'validation_enabled');
214 }
215
216 /**
217 * Set whether DataObjects should be validated before they are written.
218 *
219 * Caution: Validation can contain safeguards against invalid/malicious data,
220 * and check permission levels (e.g. on {@link Group}). Therefore it is recommended
221 * to only disable validation for very specific use cases.
222 *
223 * @param $enable bool
224 * @see DataObject::validate()
225 * @deprecated 3.2 Use the "DataObject.validation_enabled" config setting instead
226 */
227 public static function set_validation_enabled($enable) {
228 Deprecation::notice('3.2', 'Use the "DataObject.validation_enabled" config setting instead');
229 Config::inst()->update('DataObject', 'validation_enabled', (bool)$enable);
230 }
231
232 /**
233 * @var [string] - class => ClassName field definition cache for self::database_fields
234 */
235 private static $classname_spec_cache = array();
236
237 /**
238 * Clear all cached classname specs. It's necessary to clear all cached subclassed names
239 * for any classes if a new class manifest is generated.
240 */
241 public static function clear_classname_spec_cache() {
242 self::$classname_spec_cache = array();
243 PolymorphicForeignKey::clear_classname_spec_cache();
244 }
245
246 /**
247 * Determines the specification for the ClassName field for the given class
248 *
249 * @param string $class
250 * @param boolean $queryDB Determine if the DB may be queried for additional information
251 * @return string Resulting ClassName spec. If $queryDB is true this will include all
252 * legacy types that no longer have concrete classes in PHP
253 */
254 public static function get_classname_spec($class, $queryDB = true) {
255 // Check cache
256 if(!empty(self::$classname_spec_cache[$class])) return self::$classname_spec_cache[$class];
257
258 // Build known class names
259 $classNames = ClassInfo::subclassesFor($class);
260
261 // Enhance with existing classes in order to prevent legacy details being lost
262 if($queryDB && DB::get_schema()->hasField($class, 'ClassName')) {
263 $existing = DB::query("SELECT DISTINCT \"ClassName\" FROM \"{$class}\"")->column();
264 $classNames = array_unique(array_merge($classNames, $existing));
265 }
266 $spec = "Enum('" . implode(', ', $classNames) . "')";
267
268 // Only cache full information if queried
269 if($queryDB) self::$classname_spec_cache[$class] = $spec;
270 return $spec;
271 }
272
273 /**
274 * Return the complete map of fields on this object, including "Created", "LastEdited" and "ClassName".
275 * See {@link custom_database_fields()} for a getter that excludes these "base fields".
276 *
277 * @param string $class
278 * @param boolean $queryDB Determine if the DB may be queried for additional information
279 * @return array
280 */
281 public static function database_fields($class, $queryDB = true) {
282 if(get_parent_class($class) == 'DataObject') {
283 // Merge fixed with ClassName spec and custom db fields
284 $fixed = self::$fixed_fields;
285 unset($fixed['ID']);
286 return array_merge(
287 $fixed,
288 array('ClassName' => self::get_classname_spec($class, $queryDB)),
289 self::custom_database_fields($class)
290 );
291 }
292
293 return self::custom_database_fields($class);
294 }
295
296 /**
297 * Get all database columns explicitly defined on a class in {@link DataObject::$db}
298 * and {@link DataObject::$has_one}. Resolves instances of {@link CompositeDBField}
299 * into the actual database fields, rather than the name of the field which
300 * might not equate a database column.
301 *
302 * Does not include "base fields" like "ID", "ClassName", "Created", "LastEdited",
303 * see {@link database_fields()}.
304 *
305 * @uses CompositeDBField->compositeDatabaseFields()
306 *
307 * @param string $class
308 * @return array Map of fieldname to specification, similiar to {@link DataObject::$db}.
309 */
310 public static function custom_database_fields($class) {
311 if(isset(self::$_cache_custom_database_fields[$class])) {
312 return self::$_cache_custom_database_fields[$class];
313 }
314
315 $fields = Config::inst()->get($class, 'db', Config::UNINHERITED);
316
317 foreach(self::composite_fields($class, false) as $fieldName => $fieldClass) {
318 // Remove the original fieldname, it's not an actual database column
319 unset($fields[$fieldName]);
320
321 // Add all composite columns
322 $compositeFields = singleton($fieldClass)->compositeDatabaseFields();
323 if($compositeFields) foreach($compositeFields as $compositeName => $spec) {
324 $fields["{$fieldName}{$compositeName}"] = $spec;
325 }
326 }
327
328 // Add has_one relationships
329 $hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
330 if($hasOne) foreach(array_keys($hasOne) as $field) {
331
332 // Check if this is a polymorphic relation, in which case the relation
333 // is a composite field
334 if($hasOne[$field] === 'DataObject') {
335 $relationField = DBField::create_field('PolymorphicForeignKey', null, $field);
336 $relationField->setTable($class);
337 if($compositeFields = $relationField->compositeDatabaseFields()) {
338 foreach($compositeFields as $compositeName => $spec) {
339 $fields["{$field}{$compositeName}"] = $spec;
340 }
341 }
342 } else {
343 $fields[$field . 'ID'] = 'ForeignKey';
344 }
345 }
346
347 $output = (array) $fields;
348
349 self::$_cache_custom_database_fields[$class] = $output;
350
351 return $output;
352 }
353
354 /**
355 * Returns the field class if the given db field on the class is a composite field.
356 * Will check all applicable ancestor classes and aggregate results.
357 *
358 * @param string $class Class to check
359 * @param string $name Field to check
360 * @param boolean $aggregated True if parent classes should be checked, or false to limit to this class
361 * @return string Class name of composite field if it exists
362 */
363 public static function is_composite_field($class, $name, $aggregated = true) {
364 $key = $class . '_' . $name . '_' . (string)$aggregated;
365
366 if(!isset(DataObject::$_cache_is_composite_field[$key])) {
367 $isComposite = null;
368
369 if(!isset(DataObject::$_cache_composite_fields[$class])) {
370 self::cache_composite_fields($class);
371 }
372
373 if(isset(DataObject::$_cache_composite_fields[$class][$name])) {
374 $isComposite = DataObject::$_cache_composite_fields[$class][$name];
375 } elseif($aggregated && $class != 'DataObject' && ($parentClass=get_parent_class($class)) != 'DataObject') {
376 $isComposite = self::is_composite_field($parentClass, $name);
377 }
378
379 DataObject::$_cache_is_composite_field[$key] = ($isComposite) ? $isComposite : false;
380 }
381
382 return DataObject::$_cache_is_composite_field[$key] ?: null;
383 }
384
385 /**
386 * Returns a list of all the composite if the given db field on the class is a composite field.
387 * Will check all applicable ancestor classes and aggregate results.
388 */
389 public static function composite_fields($class, $aggregated = true) {
390 if(!isset(DataObject::$_cache_composite_fields[$class])) self::cache_composite_fields($class);
391
392 $compositeFields = DataObject::$_cache_composite_fields[$class];
393
394 if($aggregated && $class != 'DataObject' && ($parentClass=get_parent_class($class)) != 'DataObject') {
395 $compositeFields = array_merge($compositeFields,
396 self::composite_fields($parentClass));
397 }
398
399 return $compositeFields;
400 }
401
402 /**
403 * Internal cacher for the composite field information
404 */
405 private static function cache_composite_fields($class) {
406 $compositeFields = array();
407
408 $fields = Config::inst()->get($class, 'db', Config::UNINHERITED);
409 if($fields) foreach($fields as $fieldName => $fieldClass) {
410 if(!is_string($fieldClass)) continue;
411
412 // Strip off any parameters
413 $bPos = strpos($fieldClass, '(');
414 if($bPos !== FALSE) $fieldClass = substr($fieldClass, 0, $bPos);
415
416 // Test to see if it implements CompositeDBField
417 if(ClassInfo::classImplements($fieldClass, 'CompositeDBField')) {
418 $compositeFields[$fieldName] = $fieldClass;
419 }
420 }
421
422 DataObject::$_cache_composite_fields[$class] = $compositeFields;
423 }
424
425 /**
426 * Construct a new DataObject.
427 *
428 * @param array|null $record This will be null for a new database record. Alternatively, you can pass an array of
429 * field values. Normally this contructor is only used by the internal systems that get objects from the database.
430 * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
431 * Singletons don't have their defaults set.
432 */
433 public function __construct($record = null, $isSingleton = false, $model = null) {
434 parent::__construct();
435
436 // Set the fields data.
437 if(!$record) {
438 $record = array(
439 'ID' => 0,
440 'ClassName' => get_class($this),
441 'RecordClassName' => get_class($this)
442 );
443 }
444
445 if(!is_array($record) && !is_a($record, "stdClass")) {
446 if(is_object($record)) $passed = "an object of type '$record->class'";
447 else $passed = "The value '$record'";
448
449 user_error("DataObject::__construct passed $passed. It's supposed to be passed an array,"
450 . " taken straight from the database. Perhaps you should use DataList::create()->First(); instead?",
451 E_USER_WARNING);
452 $record = null;
453 }
454
455 if(is_a($record, "stdClass")) {
456 $record = (array)$record;
457 }
458
459 // Set $this->record to $record, but ignore NULLs
460 $this->record = array();
461 foreach($record as $k => $v) {
462 // Ensure that ID is stored as a number and not a string
463 // To do: this kind of clean-up should be done on all numeric fields, in some relatively
464 // performant manner
465 if($v !== null) {
466 if($k == 'ID' && is_numeric($v)) $this->record[$k] = (int)$v;
467 else $this->record[$k] = $v;
468 }
469 }
470
471 // Identify fields that should be lazy loaded, but only on existing records
472 if(!empty($record['ID'])) {
473 $currentObj = get_class($this);
474 while($currentObj != 'DataObject') {
475 $fields = self::custom_database_fields($currentObj);
476 foreach($fields as $field => $type) {
477 if(!array_key_exists($field, $record)) $this->record[$field.'_Lazy'] = $currentObj;
478 }
479 $currentObj = get_parent_class($currentObj);
480 }
481 }
482
483 $this->original = $this->record;
484
485 // Keep track of the modification date of all the data sourced to make this page
486 // From this we create a Last-Modified HTTP header
487 if(isset($record['LastEdited'])) {
488 HTTP::register_modification_date($record['LastEdited']);
489 }
490
491 // this must be called before populateDefaults(), as field getters on a DataObject
492 // may call getComponent() and others, which rely on $this->model being set.
493 $this->model = $model ? $model : DataModel::inst();
494
495 // Must be called after parent constructor
496 if(!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
497 $this->populateDefaults();
498 }
499
500 // prevent populateDefaults() and setField() from marking overwritten defaults as changed
501 $this->changed = array();
502 }
503
504 /**
505 * Set the DataModel
506 * @param DataModel $model
507 * @return DataObject $this
508 */
509 public function setDataModel(DataModel $model) {
510 $this->model = $model;
511 return $this;
512 }
513
514 /**
515 * Destroy all of this objects dependant objects and local caches.
516 * You'll need to call this to get the memory of an object that has components or extensions freed.
517 */
518 public function destroy() {
519 //$this->destroyed = true;
520 gc_collect_cycles();
521 $this->flushCache(false);
522 }
523
524 /**
525 * Create a duplicate of this node.
526 * Note: now also duplicates relations.
527 *
528 * @param $doWrite Perform a write() operation before returning the object. If this is true, it will create the
529 * duplicate in the database.
530 * @return DataObject A duplicate of this node. The exact type will be the type of this node.
531 */
532 public function duplicate($doWrite = true) {
533 $className = $this->class;
534 $clone = new $className( $this->toMap(), false, $this->model );
535 $clone->ID = 0;
536
537 $clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite);
538 if($doWrite) {
539 $clone->write();
540 $this->duplicateManyManyRelations($this, $clone);
541 }
542 $clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite);
543
544 return $clone;
545 }
546
547 /**
548 * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object
549 * The destinationObject must be written to the database already and have an ID. Writing is performed
550 * automatically when adding the new relations.
551 *
552 * @param $sourceObject the source object to duplicate from
553 * @param $destinationObject the destination object to populate with the duplicated relations
554 * @return DataObject with the new many_many relations copied in
555 */
556 protected function duplicateManyManyRelations($sourceObject, $destinationObject) {
557 if (!$destinationObject || $destinationObject->ID < 1) {
558 user_error("Can't duplicate relations for an object that has not been written to the database",
559 E_USER_ERROR);
560 }
561
562 //duplicate complex relations
563 // DO NOT copy has_many relations, because copying the relation would result in us changing the has_one
564 // relation on the other side of this relation to point at the copy and no longer the original (being a
565 // has_one, it can only point at one thing at a time). So, all relations except has_many can and are copied
566 if ($sourceObject->hasOne()) foreach($sourceObject->hasOne() as $name => $type) {
567 $this->duplicateRelations($sourceObject, $destinationObject, $name);
568 }
569 if ($sourceObject->manyMany()) foreach($sourceObject->manyMany() as $name => $type) {
570 //many_many include belongs_many_many
571 $this->duplicateRelations($sourceObject, $destinationObject, $name);
572 }
573
574 return $destinationObject;
575 }
576
577 /**
578 * Helper function to duplicate relations from one object to another
579 * @param $sourceObject the source object to duplicate from
580 * @param $destinationObject the destination object to populate with the duplicated relations
581 * @param $name the name of the relation to duplicate (e.g. members)
582 */
583 private function duplicateRelations($sourceObject, $destinationObject, $name) {
584 $relations = $sourceObject->$name();
585 if ($relations) {
586 if ($relations instanceOf RelationList) { //many-to-something relation
587 if ($relations->Count() > 0) { //with more than one thing it is related to
588 foreach($relations as $relation) {
589 $destinationObject->$name()->add($relation);
590 }
591 }
592 } else { //one-to-one relation
593 $destinationObject->{"{$name}ID"} = $relations->ID;
594 }
595 }
596 }
597
598 public function getObsoleteClassName() {
599 $className = $this->getField("ClassName");
600 if (!ClassInfo::exists($className)) return $className;
601 }
602
603 public function getClassName() {
604 $className = $this->getField("ClassName");
605 if (!ClassInfo::exists($className)) return get_class($this);
606 return $className;
607 }
608
609 /**
610 * Set the ClassName attribute. {@link $class} is also updated.
611 * Warning: This will produce an inconsistent record, as the object
612 * instance will not automatically switch to the new subclass.
613 * Please use {@link newClassInstance()} for this purpose,
614 * or destroy and reinstanciate the record.
615 *
616 * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
617 * @return DataObject $this
618 */
619 public function setClassName($className) {
620 $className = trim($className);
621 if(!$className || !is_subclass_of($className, 'DataObject')) return;
622
623 $this->class = $className;
624 $this->setField("ClassName", $className);
625 return $this;
626 }
627
628 /**
629 * Create a new instance of a different class from this object's record.
630 * This is useful when dynamically changing the type of an instance. Specifically,
631 * it ensures that the instance of the class is a match for the className of the
632 * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
633 * property manually before calling this method, as it will confuse change detection.
634 *
635 * If the new class is different to the original class, defaults are populated again
636 * because this will only occur automatically on instantiation of a DataObject if
637 * there is no record, or the record has no ID. In this case, we do have an ID but
638 * we still need to repopulate the defaults.
639 *
640 * @param string $newClassName The name of the new class
641 *
642 * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
643 */
644 public function newClassInstance($newClassName) {
645 $originalClass = $this->ClassName;
646 $newInstance = new $newClassName(array_merge(
647 $this->record,
648 array(
649 'ClassName' => $originalClass,
650 'RecordClassName' => $originalClass,
651 )
652 ), false, $this->model);
653
654 if($newClassName != $originalClass) {
655 $newInstance->setClassName($newClassName);
656 $newInstance->populateDefaults();
657 $newInstance->forceChange();
658 }
659
660 return $newInstance;
661 }
662
663 /**
664 * Adds methods from the extensions.
665 * Called by Object::__construct() once per class.
666 */
667 public function defineMethods() {
668 parent::defineMethods();
669
670 // Define the extra db fields - this is only necessary for extensions added in the
671 // class definition. Object::add_extension() will call this at definition time for
672 // those objects, which is a better mechanism. Perhaps extensions defined inside the
673 // class def can somehow be applied at definiton time also?
674 if($this->extension_instances) foreach($this->extension_instances as $i => $instance) {
675 if(!$instance->class) {
676 $class = get_class($instance);
677 user_error("DataObject::defineMethods(): Please ensure {$class}::__construct() calls"
678 . " parent::__construct()", E_USER_ERROR);
679 }
680 }
681
682 if($this->class == 'DataObject') return;
683
684 // Set up accessors for joined items
685 if($manyMany = $this->manyMany()) {
686 foreach($manyMany as $relationship => $class) {
687 $this->addWrapperMethod($relationship, 'getManyManyComponents');
688 }
689 }
690 if($hasMany = $this->hasMany()) {
691
692 foreach($hasMany as $relationship => $class) {
693 $this->addWrapperMethod($relationship, 'getComponents');
694 }
695
696 }
697 if($hasOne = $this->hasOne()) {
698 foreach($hasOne as $relationship => $class) {
699 $this->addWrapperMethod($relationship, 'getComponent');
700 }
701 }
702 if($belongsTo = $this->belongsTo()) foreach(array_keys($belongsTo) as $relationship) {
703 $this->addWrapperMethod($relationship, 'getComponent');
704 }
705 }
706
707 /**
708 * Returns true if this object "exists", i.e., has a sensible value.
709 * The default behaviour for a DataObject is to return true if
710 * the object exists in the database, you can override this in subclasses.
711 *
712 * @return boolean true if this object exists
713 */
714 public function exists() {
715 return (isset($this->record['ID']) && $this->record['ID'] > 0);
716 }
717
718 /**
719 * Returns TRUE if all values (other than "ID") are
720 * considered empty (by weak boolean comparison).
721 * Only checks for fields listed in {@link custom_database_fields()}
722 *
723 * @todo Use DBField->hasValue()
724 *
725 * @return boolean
726 */
727 public function isEmpty(){
728 $isEmpty = true;
729 $customFields = self::custom_database_fields(get_class($this));
730 if($map = $this->toMap()){
731 foreach($map as $k=>$v){
732 // only look at custom fields
733 if(!array_key_exists($k, $customFields)) continue;
734
735 $dbObj = ($v instanceof DBField) ? $v : $this->dbObject($k);
736 $isEmpty = ($isEmpty && !$dbObj->exists());
737 }
738 }
739 return $isEmpty;
740 }
741
742 /**
743 * Get the user friendly singular name of this DataObject.
744 * If the name is not defined (by redefining $singular_name in the subclass),
745 * this returns the class name.
746 *
747 * @return string User friendly singular name of this DataObject
748 */
749 public function singular_name() {
750 if(!$name = $this->stat('singular_name')) {
751 $name = ucwords(trim(strtolower(preg_replace('/_?([A-Z])/', ' $1', $this->class))));
752 }
753
754 return $name;
755 }
756
757 /**
758 * Get the translated user friendly singular name of this DataObject
759 * same as singular_name() but runs it through the translating function
760 *
761 * Translating string is in the form:
762 * $this->class.SINGULARNAME
763 * Example:
764 * Page.SINGULARNAME
765 *
766 * @return string User friendly translated singular name of this DataObject
767 */
768 public function i18n_singular_name() {
769 return _t($this->class.'.SINGULARNAME', $this->singular_name());
770 }
771
772 /**
773 * Get the user friendly plural name of this DataObject
774 * If the name is not defined (by renaming $plural_name in the subclass),
775 * this returns a pluralised version of the class name.
776 *
777 * @return string User friendly plural name of this DataObject
778 */
779 public function plural_name() {
780 if($name = $this->stat('plural_name')) {
781 return $name;
782 } else {
783 $name = $this->singular_name();
784 //if the penultimate character is not a vowel, replace "y" with "ies"
785 if (preg_match('/[^aeiou]y$/i', $name)) {
786 $name = substr($name,0,-1) . 'ie';
787 }
788 return ucfirst($name . 's');
789 }
790 }
791
792 /**
793 * Get the translated user friendly plural name of this DataObject
794 * Same as plural_name but runs it through the translation function
795 * Translation string is in the form:
796 * $this->class.PLURALNAME
797 * Example:
798 * Page.PLURALNAME
799 *
800 * @return string User friendly translated plural name of this DataObject
801 */
802 public function i18n_plural_name()
803 {
804 $name = $this->plural_name();
805 return _t($this->class.'.PLURALNAME', $name);
806 }
807
808 /**
809 * Standard implementation of a title/label for a specific
810 * record. Tries to find properties 'Title' or 'Name',
811 * and falls back to the 'ID'. Useful to provide
812 * user-friendly identification of a record, e.g. in errormessages
813 * or UI-selections.
814 *
815 * Overload this method to have a more specialized implementation,
816 * e.g. for an Address record this could be:
817 * <code>
818 * function getTitle() {
819 * return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
820 * }
821 * </code>
822 *
823 * @return string
824 */
825 public function getTitle() {
826 if($this->hasDatabaseField('Title')) return $this->getField('Title');
827 if($this->hasDatabaseField('Name')) return $this->getField('Name');
828
829 return "#{$this->ID}";
830 }
831
832 /**
833 * Returns the associated database record - in this case, the object itself.
834 * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
835 *
836 * @return DataObject Associated database record
837 */
838 public function data() {
839 return $this;
840 }
841
842 /**
843 * Convert this object to a map.
844 *
845 * @return array The data as a map.
846 */
847 public function toMap() {
848 $this->loadLazyFields();
849 return $this->record;
850 }
851
852 /**
853 * Return all currently fetched database fields.
854 *
855 * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
856 * Obviously, this makes it a lot faster.
857 *
858 * @return array The data as a map.
859 */
860 public function getQueriedDatabaseFields() {
861 return $this->record;
862 }
863
864 /**
865 * Update a number of fields on this object, given a map of the desired changes.
866 *
867 * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
868 * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
869 *
870 * update() doesn't write the main object, but if you use the dot syntax, it will write()
871 * the related objects that it alters.
872 *
873 * @param array $data A map of field name to data values to update.
874 * @return DataObject $this
875 */
876 public function update($data) {
877 foreach($data as $k => $v) {
878 // Implement dot syntax for updates
879 if(strpos($k,'.') !== false) {
880 $relations = explode('.', $k);
881 $fieldName = array_pop($relations);
882 $relObj = $this;
883 foreach($relations as $i=>$relation) {
884 // no support for has_many or many_many relationships,
885 // as the updater wouldn't know which object to write to (or create)
886 if($relObj->$relation() instanceof DataObject) {
887 $parentObj = $relObj;
888 $relObj = $relObj->$relation();
889 // If the intermediate relationship objects have been created, then write them
890 if($i<sizeof($relation)-1 && !$relObj->ID || (!$relObj->ID && $parentObj != $this)) {
891 $relObj->write();
892 $relatedFieldName = $relation."ID";
893 $parentObj->$relatedFieldName = $relObj->ID;
894 $parentObj->write();
895 }
896 } else {
897 user_error(
898 "DataObject::update(): Can't traverse relationship '$relation'," .
899 "it has to be a has_one relationship or return a single DataObject",
900 E_USER_NOTICE
901 );
902 // unset relation object so we don't write properties to the wrong object
903 unset($relObj);
904 break;
905 }
906 }
907
908 if($relObj) {
909 $relObj->$fieldName = $v;
910 $relObj->write();
911 $relatedFieldName = $relation."ID";
912 $this->$relatedFieldName = $relObj->ID;
913 $relObj->flushCache();
914 } else {
915 user_error("Couldn't follow dot syntax '$k' on '$this->class' object", E_USER_WARNING);
916 }
917 } else {
918 $this->$k = $v;
919 }
920 }
921 return $this;
922 }
923
924 /**
925 * Pass changes as a map, and try to
926 * get automatic casting for these fields.
927 * Doesn't write to the database. To write the data,
928 * use the write() method.
929 *
930 * @param array $data A map of field name to data values to update.
931 * @return DataObject $this
932 */
933 public function castedUpdate($data) {
934 foreach($data as $k => $v) {
935 $this->setCastedField($k,$v);
936 }
937 return $this;
938 }
939
940 /**
941 * Merges data and relations from another object of same class,
942 * without conflict resolution. Allows to specify which
943 * dataset takes priority in case its not empty.
944 * has_one-relations are just transferred with priority 'right'.
945 * has_many and many_many-relations are added regardless of priority.
946 *
947 * Caution: has_many/many_many relations are moved rather than duplicated,
948 * meaning they are not connected to the merged object any longer.
949 * Caution: Just saves updated has_many/many_many relations to the database,
950 * doesn't write the updated object itself (just writes the object-properties).
951 * Caution: Does not delete the merged object.
952 * Caution: Does now overwrite Created date on the original object.
953 *
954 * @param $obj DataObject
955 * @param $priority String left|right Determines who wins in case of a conflict (optional)
956 * @param $includeRelations Boolean Merge any existing relations (optional)
957 * @param $overwriteWithEmpty Boolean Overwrite existing left values with empty right values.
958 * Only applicable with $priority='right'. (optional)
959 * @return Boolean
960 */
961 public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false) {
962 $leftObj = $this;
963
964 if($leftObj->ClassName != $rightObj->ClassName) {
965 // we can't merge similiar subclasses because they might have additional relations
966 user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
967 (expected '{$leftObj->ClassName}').", E_USER_WARNING);
968 return false;
969 }
970
971 if(!$rightObj->ID) {
972 user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
973 to make sure all relations are transferred properly.').", E_USER_WARNING);
974 return false;
975 }
976
977 // makes sure we don't merge data like ID or ClassName
978 $leftData = $leftObj->inheritedDatabaseFields();
979 $rightData = $rightObj->inheritedDatabaseFields();
980
981 foreach($rightData as $key=>$rightVal) {
982 // don't merge conflicting values if priority is 'left'
983 if($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) continue;
984
985 // don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
986 if($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) continue;
987
988 // TODO remove redundant merge of has_one fields
989 $leftObj->{$key} = $rightObj->{$key};
990 }
991
992 // merge relations
993 if($includeRelations) {
994 if($manyMany = $this->manyMany()) {
995 foreach($manyMany as $relationship => $class) {
996 $leftComponents = $leftObj->getManyManyComponents($relationship);
997 $rightComponents = $rightObj->getManyManyComponents($relationship);
998 if($rightComponents && $rightComponents->exists()) {
999 $leftComponents->addMany($rightComponents->column('ID'));
1000 }
1001 $leftComponents->write();
1002 }
1003 }
1004
1005 if($hasMany = $this->hasMany()) {
1006 foreach($hasMany as $relationship => $class) {
1007 $leftComponents = $leftObj->getComponents($relationship);
1008 $rightComponents = $rightObj->getComponents($relationship);
1009 if($rightComponents && $rightComponents->exists()) {
1010 $leftComponents->addMany($rightComponents->column('ID'));
1011 }
1012 $leftComponents->write();
1013 }
1014
1015 }
1016
1017 if($hasOne = $this->hasOne()) {
1018 foreach($hasOne as $relationship => $class) {
1019 $leftComponent = $leftObj->getComponent($relationship);
1020 $rightComponent = $rightObj->getComponent($relationship);
1021 if($leftComponent->exists() && $rightComponent->exists() && $priority == 'right') {
1022 $leftObj->{$relationship . 'ID'} = $rightObj->{$relationship . 'ID'};
1023 }
1024 }
1025 }
1026 }
1027
1028 return true;
1029 }
1030
1031 /**
1032 * Forces the record to think that all its data has changed.
1033 * Doesn't write to the database. Only sets fields as changed
1034 * if they are not already marked as changed.
1035 *
1036 * @return $this
1037 */
1038 public function forceChange() {
1039 // Ensure lazy fields loaded
1040 $this->loadLazyFields();
1041
1042 // $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
1043 $fieldNames = array_unique(array_merge(
1044 array_keys($this->record),
1045 array_keys($this->inheritedDatabaseFields())
1046 ));
1047
1048 foreach($fieldNames as $fieldName) {
1049 if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = self::CHANGE_STRICT;
1050 // Populate the null values in record so that they actually get written
1051 if(!isset($this->record[$fieldName])) $this->record[$fieldName] = null;
1052 }
1053
1054 // @todo Find better way to allow versioned to write a new version after forceChange
1055 if($this->isChanged('Version')) unset($this->changed['Version']);
1056 return $this;
1057 }
1058
1059 /**
1060 * Validate the current object.
1061 *
1062 * By default, there is no validation - objects are always valid! However, you can overload this method in your
1063 * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1064 *
1065 * Invalid objects won't be able to be written - a warning will be thrown and no write will occur. onBeforeWrite()
1066 * and onAfterWrite() won't get called either.
1067 *
1068 * It is expected that you call validate() in your own application to test that an object is valid before
1069 * attempting a write, and respond appropriately if it isn't.
1070 *
1071 * @see {@link ValidationResult}
1072 * @return ValidationResult
1073 */
1074 protected function validate() {
1075 $result = ValidationResult::create();
1076 $this->extend('validate', $result);
1077 return $result;
1078 }
1079
1080 /**
1081 * Public accessor for {@see DataObject::validate()}
1082 *
1083 * @return ValidationResult
1084 */
1085 public function doValidate() {
1086 // validate will be public in 4.0
1087 return $this->validate();
1088 }
1089
1090 /**
1091 * Event handler called before writing to the database.
1092 * You can overload this to clean up or otherwise process data before writing it to the
1093 * database. Don't forget to call parent::onBeforeWrite(), though!
1094 *
1095 * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1096 *
1097 * @uses DataExtension->onBeforeWrite()
1098 */
1099 protected function onBeforeWrite() {
1100 $this->brokenOnWrite = false;
1101
1102 $dummy = null;
1103 $this->extend('onBeforeWrite', $dummy);
1104 }
1105
1106 /**
1107 * Event handler called after writing to the database.
1108 * You can overload this to act upon changes made to the data after it is written.
1109 * $this->changed will have a record
1110 * database. Don't forget to call parent::onAfterWrite(), though!
1111 *
1112 * @uses DataExtension->onAfterWrite()
1113 */
1114 protected function onAfterWrite() {
1115 $dummy = null;
1116 $this->extend('onAfterWrite', $dummy);
1117 }
1118
1119 /**
1120 * Event handler called before deleting from the database.
1121 * You can overload this to clean up or otherwise process data before delete this
1122 * record. Don't forget to call parent::onBeforeDelete(), though!
1123 *
1124 * @uses DataExtension->onBeforeDelete()
1125 */
1126 protected function onBeforeDelete() {
1127 $this->brokenOnDelete = false;
1128
1129 $dummy = null;
1130 $this->extend('onBeforeDelete', $dummy);
1131 }
1132
1133 protected function onAfterDelete() {
1134 $this->extend('onAfterDelete');
1135 }
1136
1137 /**
1138 * Load the default values in from the self::$defaults array.
1139 * Will traverse the defaults of the current class and all its parent classes.
1140 * Called by the constructor when creating new records.
1141 *
1142 * @uses DataExtension->populateDefaults()
1143 * @return DataObject $this
1144 */
1145 public function populateDefaults() {
1146 $classes = array_reverse(ClassInfo::ancestry($this));
1147
1148 foreach($classes as $class) {
1149 $defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1150
1151 if($defaults && !is_array($defaults)) {
1152 user_error("Bad '$this->class' defaults given: " . var_export($defaults, true),
1153 E_USER_WARNING);
1154 $defaults = null;
1155 }
1156
1157 if($defaults) foreach($defaults as $fieldName => $fieldValue) {
1158 // SRM 2007-03-06: Stricter check
1159 if(!isset($this->$fieldName) || $this->$fieldName === null) {
1160 $this->$fieldName = $fieldValue;
1161 }
1162 // Set many-many defaults with an array of ids
1163 if(is_array($fieldValue) && $this->manyManyComponent($fieldName)) {
1164 $manyManyJoin = $this->$fieldName();
1165 $manyManyJoin->setByIdList($fieldValue);
1166 }
1167 }
1168 if($class == 'DataObject') {
1169 break;
1170 }
1171 }
1172
1173 $this->extend('populateDefaults');
1174 return $this;
1175 }
1176
1177 /**
1178 * Determine validation of this object prior to write
1179 *
1180 * @return ValidationException Exception generated by this write, or null if valid
1181 */
1182 protected function validateWrite() {
1183 if ($this->ObsoleteClassName) {
1184 return new ValidationException(
1185 "Object is of class '{$this->ObsoleteClassName}' which doesn't exist - ".
1186 "you need to change the ClassName before you can write it",
1187 E_USER_WARNING
1188 );
1189 }
1190
1191 if(Config::inst()->get('DataObject', 'validation_enabled')) {
1192 $result = $this->validate();
1193 if (!$result->valid()) {
1194 return new ValidationException(
1195 $result,
1196 $result->message(),
1197 E_USER_WARNING
1198 );
1199 }
1200 }
1201 }
1202
1203 /**
1204 * Prepare an object prior to write
1205 *
1206 * @throws ValidationException
1207 */
1208 protected function preWrite() {
1209 // Validate this object
1210 if($writeException = $this->validateWrite()) {
1211 // Used by DODs to clean up after themselves, eg, Versioned
1212 $this->invokeWithExtensions('onAfterSkippedWrite');
1213 throw $writeException;
1214 }
1215
1216 // Check onBeforeWrite
1217 $this->brokenOnWrite = true;
1218 $this->onBeforeWrite();
1219 if($this->brokenOnWrite) {
1220 user_error("$this->class has a broken onBeforeWrite() function."
1221 . " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1222 }
1223 }
1224
1225 /**
1226 * Detects and updates all changes made to this object
1227 *
1228 * @param bool $forceChanges If set to true, force all fields to be treated as changed
1229 * @return bool True if any changes are detected
1230 */
1231 protected function updateChanges($forceChanges = false)
1232 {
1233 if($forceChanges) {
1234 // Force changes, but only for loaded fields
1235 foreach($this->record as $field => $value) {
1236 $this->changed[$field] = static::CHANGE_VALUE;
1237 }
1238 return true;
1239 }
1240 return $this->isChanged();
1241 }
1242
1243 /**
1244 * Writes a subset of changes for a specific table to the given manipulation
1245 *
1246 * @param string $baseTable Base table
1247 * @param string $now Timestamp to use for the current time
1248 * @param bool $isNewRecord Whether this should be treated as a new record write
1249 * @param array $manipulation Manipulation to write to
1250 * @param string $class Table and Class to select and write to
1251 */
1252 protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class) {
1253 $manipulation[$class] = array();
1254
1255 // Extract records for this table
1256 foreach($this->record as $fieldName => $fieldValue) {
1257
1258 // Check if this record pertains to this table, and
1259 // we're not attempting to reset the BaseTable->ID
1260 if( empty($this->changed[$fieldName])
1261 || ($class === $baseTable && $fieldName === 'ID')
1262 || (!self::has_own_table_database_field($class, $fieldName)
1263 && !self::is_composite_field($class, $fieldName, false))
1264 ) {
1265 continue;
1266 }
1267
1268
1269 // if database column doesn't correlate to a DBField instance...
1270 $fieldObj = $this->dbObject($fieldName);
1271 if(!$fieldObj) {
1272 $fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1273 }
1274
1275 // Ensure DBField is repopulated and written to the manipulation
1276 $fieldObj->setValue($fieldValue, $this->record);
1277 $fieldObj->writeToManipulation($manipulation[$class]);
1278 }
1279
1280 // Ensure update of Created and LastEdited columns
1281 if($baseTable === $class) {
1282 $manipulation[$class]['fields']['LastEdited'] = $now;
1283 if($isNewRecord) {
1284 $manipulation[$class]['fields']['Created']
1285 = empty($this->record['Created'])
1286 ? $now
1287 : $this->record['Created'];
1288 $manipulation[$class]['fields']['ClassName'] = $this->class;
1289 }
1290 }
1291
1292 // Inserts done one the base table are performed in another step, so the manipulation should instead
1293 // attempt an update, as though it were a normal update.
1294 $manipulation[$class]['command'] = $isNewRecord ? 'insert' : 'update';
1295 $manipulation[$class]['id'] = $this->record['ID'];
1296 }
1297
1298 /**
1299 * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1300 *
1301 * Does nothing if an ID is already assigned for this record
1302 *
1303 * @param string $baseTable Base table
1304 * @param string $now Timestamp to use for the current time
1305 */
1306 protected function writeBaseRecord($baseTable, $now) {
1307 // Generate new ID if not specified
1308 if($this->isInDB()) return;
1309
1310 // Perform an insert on the base table
1311 $insert = new SQLInsert('"'.$baseTable.'"');
1312 $insert
1313 ->assign('"Created"', $now)
1314 ->execute();
1315 $this->changed['ID'] = self::CHANGE_VALUE;
1316 $this->record['ID'] = DB::get_generated_id($baseTable);
1317 }
1318
1319 /**
1320 * Generate and write the database manipulation for all changed fields
1321 *
1322 * @param string $baseTable Base table
1323 * @param string $now Timestamp to use for the current time
1324 * @param bool $isNewRecord If this is a new record
1325 */
1326 protected function writeManipulation($baseTable, $now, $isNewRecord) {
1327 // Generate database manipulations for each class
1328 $manipulation = array();
1329 foreach($this->getClassAncestry() as $class) {
1330 if(self::has_own_table($class)) {
1331 $this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1332 }
1333 }
1334
1335 // Allow extensions to extend this manipulation
1336 $this->extend('augmentWrite', $manipulation);
1337
1338 // New records have their insert into the base data table done first, so that they can pass the
1339 // generated ID on to the rest of the manipulation
1340 if($isNewRecord) {
1341 $manipulation[$baseTable]['command'] = 'update';
1342 }
1343
1344 // Perform the manipulation
1345 DB::manipulate($manipulation);
1346 }
1347
1348 /**
1349 * Writes all changes to this object to the database.
1350 * - It will insert a record whenever ID isn't set, otherwise update.
1351 * - All relevant tables will be updated.
1352 * - $this->onBeforeWrite() gets called beforehand.
1353 * - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1354 *
1355 * @uses DataExtension->augmentWrite()
1356 *
1357 * @param boolean $showDebug Show debugging information
1358 * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1359 * @param boolean $forceWrite Write to database even if there are no changes
1360 * @param boolean $writeComponents Call write() on all associated component instances which were previously
1361 * retrieved through {@link getComponent()}, {@link getComponents()} or
1362 * {@link getManyManyComponents()} (Default: false)
1363 * @return int The ID of the record
1364 * @throws ValidationException Exception that can be caught and handled by the calling function
1365 */
1366 public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false) {
1367 $now = SS_Datetime::now()->Rfc2822();
1368
1369 // Execute pre-write tasks
1370 $this->preWrite();
1371
1372 // Check if we are doing an update or an insert
1373 $isNewRecord = !$this->isInDB() || $forceInsert;
1374
1375 // Check changes exist, abort if there are none
1376 $hasChanges = $this->updateChanges($isNewRecord);
1377 if($hasChanges || $forceWrite || $isNewRecord) {
1378 // New records have their insert into the base data table done first, so that they can pass the
1379 // generated primary key on to the rest of the manipulation
1380 $baseTable = ClassInfo::baseDataClass($this->class);
1381 $this->writeBaseRecord($baseTable, $now);
1382
1383 // Write the DB manipulation for all changed fields
1384 $this->writeManipulation($baseTable, $now, $isNewRecord);
1385
1386 // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1387 $this->writeRelations();
1388 $this->onAfterWrite();
1389 $this->changed = array();
1390 } else {
1391 if($showDebug) Debug::message("no changes for DataObject");
1392
1393 // Used by DODs to clean up after themselves, eg, Versioned
1394 $this->invokeWithExtensions('onAfterSkippedWrite');
1395 }
1396
1397 // Ensure Created and LastEdited are populated
1398 if(!isset($this->record['Created'])) {
1399 $this->record['Created'] = $now;
1400 }
1401 $this->record['LastEdited'] = $now;
1402
1403 // Write relations as necessary
1404 if($writeComponents) $this->writeComponents(true);
1405
1406 // Clears the cache for this object so get_one returns the correct object.
1407 $this->flushCache();
1408
1409 return $this->record['ID'];
1410 }
1411
1412 /**
1413 * Writes cached relation lists to the database, if possible
1414 */
1415 public function writeRelations() {
1416 if(!$this->isInDB()) return;
1417
1418 // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1419 if($this->unsavedRelations) {
1420 foreach($this->unsavedRelations as $name => $list) {
1421 $list->changeToList($this->$name());
1422 }
1423 $this->unsavedRelations = array();
1424 }
1425 }
1426
1427 /**
1428 * Write the cached components to the database. Cached components could refer to two different instances of the
1429 * same record.
1430 *
1431 * @param $recursive Recursively write components
1432 * @return DataObject $this
1433 */
1434 public function writeComponents($recursive = false) {
1435 if(!$this->components) return $this;
1436
1437 foreach($this->components as $component) {
1438 $component->write(false, false, false, $recursive);
1439 }
1440 return $this;
1441 }
1442
1443 /**
1444 * Delete this data object.
1445 * $this->onBeforeDelete() gets called.
1446 * Note that in Versioned objects, both Stage and Live will be deleted.
1447 * @uses DataExtension->augmentSQL()
1448 */
1449 public function delete() {
1450 $this->brokenOnDelete = true;
1451 $this->onBeforeDelete();
1452 if($this->brokenOnDelete) {
1453 user_error("$this->class has a broken onBeforeDelete() function."
1454 . " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1455 }
1456
1457 // Deleting a record without an ID shouldn't do anything
1458 if(!$this->ID) throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1459
1460 // TODO: This is quite ugly. To improve:
1461 // - move the details of the delete code in the DataQuery system
1462 // - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1463 // obviously, that means getting requireTable() to configure cascading deletes ;-)
1464 $srcQuery = DataList::create($this->class, $this->model)->where("ID = $this->ID")->dataQuery()->query();
1465 foreach($srcQuery->queriedTables() as $table) {
1466 $delete = new SQLDelete("\"$table\"", array('"ID"' => $this->ID));
1467 $delete->execute();
1468 }
1469 // Remove this item out of any caches
1470 $this->flushCache();
1471
1472 $this->onAfterDelete();
1473
1474 $this->OldID = $this->ID;
1475 $this->ID = 0;
1476 }
1477
1478 /**
1479 * Delete the record with the given ID.
1480 *
1481 * @param string $className The class name of the record to be deleted
1482 * @param int $id ID of record to be deleted
1483 */
1484 public static function delete_by_id($className, $id) {
1485 $obj = DataObject::get_by_id($className, $id);
1486 if($obj) {
1487 $obj->delete();
1488 } else {
1489 user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1490 }
1491 }
1492
1493 /**
1494 * Get the class ancestry, including the current class name.
1495 * The ancestry will be returned as an array of class names, where the 0th element
1496 * will be the class that inherits directly from DataObject, and the last element
1497 * will be the current class.
1498 *
1499 * @return array Class ancestry
1500 */
1501 public function getClassAncestry() {
1502 if(!isset(DataObject::$_cache_get_class_ancestry[$this->class])) {
1503 DataObject::$_cache_get_class_ancestry[$this->class] = array($this->class);
1504 while(($class=get_parent_class(DataObject::$_cache_get_class_ancestry[$this->class][0])) != "DataObject") {
1505 array_unshift(DataObject::$_cache_get_class_ancestry[$this->class], $class);
1506 }
1507 }
1508 return DataObject::$_cache_get_class_ancestry[$this->class];
1509 }
1510
1511 /**
1512 * Return a component object from a one to one relationship, as a DataObject.
1513 * If no component is available, an 'empty component' will be returned for
1514 * non-polymorphic relations, or for polymorphic relations with a class set.
1515 *
1516 * @param string $componentName Name of the component
1517 *
1518 * @return DataObject The component object. It's exact type will be that of the component.
1519 */
1520 public function getComponent($componentName) {
1521 if(isset($this->components[$componentName])) {
1522 return $this->components[$componentName];
1523 }
1524
1525 if($class = $this->hasOneComponent($componentName)) {
1526 $joinField = $componentName . 'ID';
1527 $joinID = $this->getField($joinField);
1528
1529 // Extract class name for polymorphic relations
1530 if($class === 'DataObject') {
1531 $class = $this->getField($componentName . 'Class');
1532 if(empty($class)) return null;
1533 }
1534
1535 if($joinID) {
1536 $component = DataObject::get_by_id($class, $joinID);
1537 }
1538
1539 if(empty($component)) {
1540 $component = $this->model->$class->newObject();
1541 }
1542 } elseif($class = $this->belongsToComponent($componentName)) {
1543
1544 $joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
1545 $joinID = $this->ID;
1546
1547 if($joinID) {
1548
1549 $filter = $polymorphic
1550 ? array(
1551 "{$joinField}ID" => $joinID,
1552 "{$joinField}Class" => $this->class
1553 )
1554 : array(
1555 $joinField => $joinID
1556 );
1557 $component = DataObject::get($class)->filter($filter)->first();
1558 }
1559
1560 if(empty($component)) {
1561 $component = $this->model->$class->newObject();
1562 if($polymorphic) {
1563 $component->{$joinField.'ID'} = $this->ID;
1564 $component->{$joinField.'Class'} = $this->class;
1565 } else {
1566 $component->$joinField = $this->ID;
1567 }
1568 }
1569 } else {
1570 throw new Exception("DataObject->getComponent(): Could not find component '$componentName'.");
1571 }
1572
1573 $this->components[$componentName] = $component;
1574 return $component;
1575 }
1576
1577 /**
1578 * Returns a one-to-many relation as a HasManyList
1579 *
1580 * @param string $componentName Name of the component
1581 * @param string|null $filter Deprecated. A filter to be inserted into the WHERE clause
1582 * @param string|null|array $sort Deprecated. A sort expression to be inserted into the ORDER BY clause. If omitted,
1583 * the static field $default_sort on the component class will be used.
1584 * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
1585 * @param string|null|array $limit Deprecated. A limit expression to be inserted into the LIMIT clause
1586 *
1587 * @return HasManyList The components of the one-to-many relationship.
1588 */
1589 public function getComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) {
1590 $result = null;
1591
1592 if(!$componentClass = $this->hasManyComponent($componentName)) {
1593 user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName'"
1594 . " on class '$this->class'", E_USER_ERROR);
1595 }
1596
1597 if($join) {
1598 throw new \InvalidArgumentException(
1599 'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
1600 );
1601 }
1602
1603 if($filter !== null || $sort !== null || $limit !== null) {
1604 Deprecation::notice('4.0', 'The $filter, $sort and $limit parameters for DataObject::getComponents()
1605 have been deprecated. Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
1606 }
1607
1608 // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1609 if(!$this->ID) {
1610 if(!isset($this->unsavedRelations[$componentName])) {
1611 $this->unsavedRelations[$componentName] =
1612 new UnsavedRelationList($this->class, $componentName, $componentClass);
1613 }
1614 return $this->unsavedRelations[$componentName];
1615 }
1616
1617 // Determine type and nature of foreign relation
1618 $joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic);
1619 if($polymorphic) {
1620 $result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
1621 } else {
1622 $result = HasManyList::create($componentClass, $joinField);
1623 }
1624
1625 if($this->model) $result->setDataModel($this->model);
1626
1627 return $result
1628 ->forForeignID($this->ID)
1629 ->where($filter)
1630 ->limit($limit)
1631 ->sort($sort);
1632 }
1633
1634 /**
1635 * @deprecated
1636 */
1637 public function getComponentsQuery($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
1638 Deprecation::notice('4.0', "Use getComponents to get a filtered DataList for an object's relation");
1639 return $this->getComponents($componentName, $filter, $sort, $join, $limit);
1640 }
1641
1642 /**
1643 * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1644 *
1645 * @param $relationName Relation name.
1646 * @return string Class name, or null if not found.
1647 */
1648 public function getRelationClass($relationName) {
1649 // Go through all relationship configuration fields.
1650 $candidates = array_merge(
1651 ($relations = Config::inst()->get($this->class, 'has_one')) ? $relations : array(),
1652 ($relations = Config::inst()->get($this->class, 'has_many')) ? $relations : array(),
1653 ($relations = Config::inst()->get($this->class, 'many_many')) ? $relations : array(),
1654 ($relations = Config::inst()->get($this->class, 'belongs_many_many')) ? $relations : array(),
1655 ($relations = Config::inst()->get($this->class, 'belongs_to')) ? $relations : array()
1656 );
1657
1658 if (isset($candidates[$relationName])) {
1659 $remoteClass = $candidates[$relationName];
1660
1661 // If dot notation is present, extract just the first part that contains the class.
1662 if(($fieldPos = strpos($remoteClass, '.'))!==false) {
1663 return substr($remoteClass, 0, $fieldPos);
1664 }
1665
1666 // Otherwise just return the class
1667 return $remoteClass;
1668 }
1669
1670 return null;
1671 }
1672
1673 /**
1674 * Tries to find the database key on another object that is used to store a
1675 * relationship to this class. If no join field can be found it defaults to 'ParentID'.
1676 *
1677 * If the remote field is polymorphic then $polymorphic is set to true, and the return value
1678 * is in the form 'Relation' instead of 'RelationID', referencing the composite DBField.
1679 *
1680 * @param string $component Name of the relation on the current object pointing to the
1681 * remote object.
1682 * @param string $type the join type - either 'has_many' or 'belongs_to'
1683 * @param boolean $polymorphic Flag set to true if the remote join field is polymorphic.
1684 * @return string
1685 */
1686 public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
1687 // Extract relation from current object
1688 if($type === 'has_many') {
1689 $remoteClass = $this->hasManyComponent($component, false);
1690 } else {
1691 $remoteClass = $this->belongsToComponent($component, false);
1692 }
1693
1694 if(empty($remoteClass)) {
1695 throw new Exception("Unknown $type component '$component' on class '$this->class'");
1696 }
1697 if(!ClassInfo::exists(strtok($remoteClass, '.'))) {
1698 throw new Exception(
1699 "Class '$remoteClass' not found, but used in $type component '$component' on class '$this->class'"
1700 );
1701 }
1702
1703 // If presented with an explicit field name (using dot notation) then extract field name
1704 $remoteField = null;
1705 if(strpos($remoteClass, '.') !== false) {
1706 list($remoteClass, $remoteField) = explode('.', $remoteClass);
1707 }
1708
1709 // Reference remote has_one to check against
1710 $remoteRelations = Config::inst()->get($remoteClass, 'has_one');
1711
1712 // Without an explicit field name, attempt to match the first remote field
1713 // with the same type as the current class
1714 if(empty($remoteField)) {
1715 // look for remote has_one joins on this class or any parent classes
1716 $remoteRelationsMap = array_flip($remoteRelations);
1717 foreach(array_reverse(ClassInfo::ancestry($this)) as $class) {
1718 if(array_key_exists($class, $remoteRelationsMap)) {
1719 $remoteField = $remoteRelationsMap[$class];
1720 break;
1721 }
1722 }
1723 }
1724
1725 // In case of an indeterminate remote field show an error
1726 if(empty($remoteField)) {
1727 $polymorphic = false;
1728 $message = "No has_one found on class '$remoteClass'";
1729 if($type == 'has_many') {
1730 // include a hint for has_many that is missing a has_one
1731 $message .= ", the has_many relation from '$this->class' to '$remoteClass'";
1732 $message .= " requires a has_one on '$remoteClass'";
1733 }
1734 throw new Exception($message);
1735 }
1736
1737 // If given an explicit field name ensure the related class specifies this
1738 if(empty($remoteRelations[$remoteField])) {
1739 throw new Exception("Missing expected has_one named '$remoteField'
1740 on class '$remoteClass' referenced by $type named '$component'
1741 on class {$this->class}"
1742 );
1743 }
1744
1745 // Inspect resulting found relation
1746 if($remoteRelations[$remoteField] === 'DataObject') {
1747 $polymorphic = true;
1748 return $remoteField; // Composite polymorphic field does not include 'ID' suffix
1749 } else {
1750 $polymorphic = false;
1751 return $remoteField . 'ID';
1752 }
1753 }
1754
1755 /**
1756 * Returns a many-to-many component, as a ManyManyList.
1757 * @param string $componentName Name of the many-many component
1758 * @return ManyManyList The set of components
1759 *
1760 * @todo Implement query-params
1761 */
1762 public function getManyManyComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) {
1763 list($parentClass, $componentClass, $parentField, $componentField, $table)
1764 = $this->manyManyComponent($componentName);
1765
1766 if($filter !== null || $sort !== null || $join !== null || $limit !== null) {
1767 Deprecation::notice('4.0', 'The $filter, $sort, $join and $limit parameters for
1768 DataObject::getManyManyComponents() have been deprecated.
1769 Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
1770 }
1771
1772 // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1773 if(!$this->ID) {
1774 if(!isset($this->unsavedRelations[$componentName])) {
1775 $this->unsavedRelations[$componentName] =
1776 new UnsavedRelationList($parentClass, $componentName, $componentClass);
1777 }
1778 return $this->unsavedRelations[$componentName];
1779 }
1780
1781 $extraFields = $this->manyManyExtraFieldsForComponent($componentName) ?: array();
1782 $result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
1783
1784
1785 // Store component data in query meta-data
1786 $result = $result->alterDataQuery(function($query) use ($extraFields) {
1787 $query->setQueryParam('Component.ExtraFields', $extraFields);
1788 });
1789
1790 if($this->model) $result->setDataModel($this->model);
1791
1792 $this->extend('updateManyManyComponents', $result);
1793
1794 // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1795 // foreignID set elsewhere.
1796 return $result
1797 ->forForeignID($this->ID)
1798 ->where($filter)
1799 ->sort($sort)
1800 ->limit($limit);
1801 }
1802
1803 /**
1804 * @deprecated 4.0 Method has been replaced by hasOne() and hasOneComponent()
1805 * @param string $component
1806 * @return array|null
1807 */
1808 public function has_one($component = null) {
1809 if($component) {
1810 Deprecation::notice('4.0', 'Please use hasOneComponent() instead');
1811 return $this->hasOneComponent($component);
1812 }
1813
1814 Deprecation::notice('4.0', 'Please use hasOne() instead');
1815 return $this->hasOne();
1816 }
1817
1818 /**
1819 * Return the class of a one-to-one component. If $component is null, return all of the one-to-one components and
1820 * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1821 *
1822 * @param string $component Deprecated - Name of component
1823 * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1824 * their classes.
1825 */
1826 public function hasOne($component = null) {
1827 if($component) {
1828 Deprecation::notice(
1829 '4.0',
1830 'Please use DataObject::hasOneComponent() instead of passing a component name to hasOne()',
1831 Deprecation::SCOPE_GLOBAL
1832 );
1833 return $this->hasOneComponent($component);
1834 }
1835
1836 return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
1837 }
1838
1839 /**
1840 * Return data for a specific has_one component.
1841 * @param string $component
1842 * @return string|null
1843 */
1844 public function hasOneComponent($component) {
1845 $hasOnes = (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
1846
1847 if(isset($hasOnes[$component])) {
1848 return $hasOnes[$component];
1849 }
1850 }
1851
1852 /**
1853 * @deprecated 4.0 Method has been replaced by belongsTo() and belongsToComponent()
1854 * @param string $component
1855 * @param bool $classOnly
1856 * @return array|null
1857 */
1858 public function belongs_to($component = null, $classOnly = true) {
1859 if($component) {
1860 Deprecation::notice('4.0', 'Please use belongsToComponent() instead');
1861 return $this->belongsToComponent($component, $classOnly);
1862 }
1863
1864 Deprecation::notice('4.0', 'Please use belongsTo() instead');
1865 return $this->belongsTo(null, $classOnly);
1866 }
1867
1868 /**
1869 * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1870 * their class name will be returned.
1871 *
1872 * @param string $component - Name of component
1873 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1874 * the field data stripped off. It defaults to TRUE.
1875 * @return string|array
1876 */
1877 public function belongsTo($component = null, $classOnly = true) {
1878 if($component) {
1879 Deprecation::notice(
1880 '4.0',
1881 'Please use DataObject::belongsToComponent() instead of passing a component name to belongsTo()',
1882 Deprecation::SCOPE_GLOBAL
1883 );
1884 return $this->belongsToComponent($component, $classOnly);
1885 }
1886
1887 $belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1888 if($belongsTo && $classOnly) {
1889 return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1890 } else {
1891 return $belongsTo ? $belongsTo : array();
1892 }
1893 }
1894
1895 /**
1896 * Return data for a specific belongs_to component.
1897 * @param string $component
1898 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1899 * the field data stripped off. It defaults to TRUE.
1900 * @return string|false
1901 */
1902 public function belongsToComponent($component, $classOnly = true) {
1903 $belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1904
1905 if($belongsTo && array_key_exists($component, $belongsTo)) {
1906 $belongsTo = $belongsTo[$component];
1907 } else {
1908 return false;
1909 }
1910
1911 return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $belongsTo) : $belongsTo;
1912 }
1913
1914 /**
1915 * Return all of the database fields defined in self::$db and all the parent classes.
1916 * Doesn't include any fields specified by self::$has_one. Use $this->hasOne() to get these fields
1917 *
1918 * @param string $fieldName Limit the output to a specific field name
1919 * @return array The database fields
1920 */
1921 public function db($fieldName = null) {
1922 $classes = ClassInfo::ancestry($this, true);
1923
1924 // If we're looking for a specific field, we want to hit subclasses first as they may override field types
1925 if($fieldName) {
1926 $classes = array_reverse($classes);
1927 }
1928
1929 $items = array();
1930 foreach($classes as $class) {
1931 if(isset(self::$_cache_db[$class])) {
1932 $dbItems = self::$_cache_db[$class];
1933 } else {
1934 $dbItems = (array) Config::inst()->get($class, 'db', Config::UNINHERITED);
1935 self::$_cache_db[$class] = $dbItems;
1936 }
1937
1938 if($fieldName) {
1939 if(isset($dbItems[$fieldName])) {
1940 return $dbItems[$fieldName];
1941 }
1942 } else {
1943 $items = isset($items) ? array_merge((array) $items, $dbItems) : $dbItems;
1944 }
1945 }
1946
1947 return $items;
1948 }
1949
1950 /**
1951 * @deprecated 4.0 Method has been replaced by hasMany() and hasManyComponent()
1952 * @param string $component
1953 * @param bool $classOnly
1954 * @return array|null
1955 */
1956 public function has_many($component = null, $classOnly = true) {
1957 if($component) {
1958 Deprecation::notice('4.0', 'Please use hasManyComponent() instead');
1959 return $this->hasManyComponent($component, $classOnly);
1960 }
1961
1962 Deprecation::notice('4.0', 'Please use hasMany() instead');
1963 return $this->hasMany(null, $classOnly);
1964 }
1965
1966 /**
1967 * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
1968 * relationships and their classes will be returned.
1969 *
1970 * @param string $component Deprecated - Name of component
1971 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1972 * the field data stripped off. It defaults to TRUE.
1973 * @return string|array|false
1974 */
1975 public function hasMany($component = null, $classOnly = true) {
1976 if($component) {
1977 Deprecation::notice(
1978 '4.0',
1979 'Please use DataObject::hasManyComponent() instead of passing a component name to hasMany()',
1980 Deprecation::SCOPE_GLOBAL
1981 );
1982 return $this->hasManyComponent($component, $classOnly);
1983 }
1984
1985 $hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
1986 if($hasMany && $classOnly) {
1987 return preg_replace('/(.+)?\..+/', '$1', $hasMany);
1988 } else {
1989 return $hasMany ? $hasMany : array();
1990 }
1991 }
1992
1993 /**
1994 * Return data for a specific has_many component.
1995 * @param string $component
1996 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1997 * the field data stripped off. It defaults to TRUE.
1998 * @return string|false
1999 */
2000 public function hasManyComponent($component, $classOnly = true) {
2001 $hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
2002
2003 if($hasMany && array_key_exists($component, $hasMany)) {
2004 $hasMany = $hasMany[$component];
2005 } else {
2006 return false;
2007 }
2008
2009 return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $hasMany) : $hasMany;
2010 }
2011
2012 /**
2013 * @deprecated 4.0 Method has been replaced by manyManyExtraFields() and
2014 * manyManyExtraFieldsForComponent()
2015 * @param string $component
2016 * @return array
2017 */
2018 public function many_many_extraFields($component = null) {
2019 if($component) {
2020 Deprecation::notice('4.0', 'Please use manyManyExtraFieldsForComponent() instead');
2021 return $this->manyManyExtraFieldsForComponent($component);
2022 }
2023
2024 Deprecation::notice('4.0', 'Please use manyManyExtraFields() instead');
2025 return $this->manyManyExtraFields();
2026 }
2027
2028 /**
2029 * Return the many-to-many extra fields specification.
2030 *
2031 * If you don't specify a component name, it returns all
2032 * extra fields for all components available.
2033 *
2034 * @param string $component Deprecated - Name of component
2035 * @return array|null
2036 */
2037 public function manyManyExtraFields($component = null) {
2038 if($component) {
2039 Deprecation::notice(
2040 '4.0',
2041 'Please use DataObject::manyManyExtraFieldsForComponent() instead of passing a component name
2042 to manyManyExtraFields()',
2043 Deprecation::SCOPE_GLOBAL
2044 );
2045 return $this->manyManyExtraFieldsForComponent($component);
2046 }
2047
2048 return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2049 }
2050
2051 /**
2052 * Return the many-to-many extra fields specification for a specific component.
2053 * @param string $component
2054 * @return array|null
2055 */
2056 public function manyManyExtraFieldsForComponent($component) {
2057 // Get all many_many_extraFields defined in this class or parent classes
2058 $extraFields = (array)Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2059 // Extra fields are immediately available
2060 if(isset($extraFields[$component])) {
2061 return $extraFields[$component];
2062 }
2063
2064 // Check this class' belongs_many_manys to see if any of their reverse associations contain extra fields
2065 $manyMany = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2066 $candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2067 if($candidate) {
2068 $relationName = null;
2069 // Extract class and relation name from dot-notation
2070 if(strpos($candidate, '.') !== false) {
2071 list($candidate, $relationName) = explode('.', $candidate, 2);
2072 }
2073
2074 // If we've not already found the relation name from dot notation, we need to find a relation that points
2075 // back to this class. As there's no dot-notation, there can only be one relation pointing to this class,
2076 // so it's safe to assume that it's the correct one
2077 if(!$relationName) {
2078 $candidateManyManys = (array)Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2079
2080 foreach($candidateManyManys as $relation => $relatedClass) {
2081 if (is_a($this, $relatedClass)) {
2082 $relationName = $relation;
2083 }
2084 }
2085 }
2086
2087 // If we've found a matching relation on the target class, see if we can find extra fields for it
2088 $extraFields = (array)Config::inst()->get($candidate, 'many_many_extraFields', Config::UNINHERITED);
2089 if(isset($extraFields[$relationName])) {
2090 return $extraFields[$relationName];
2091 }
2092 }
2093
2094 return isset($items) ? $items : null;
2095 }
2096
2097 /**
2098 * @deprecated 4.0 Method has been renamed to manyMany()
2099 * @param string $component
2100 * @return array|null
2101 */
2102 public function many_many($component = null) {
2103 if($component) {
2104 Deprecation::notice('4.0', 'Please use manyManyComponent() instead');
2105 return $this->manyManyComponent($component);
2106 }
2107
2108 Deprecation::notice('4.0', 'Please use manyMany() instead');
2109 return $this->manyMany();
2110 }
2111
2112 /**
2113 * Return information about a many-to-many component.
2114 * The return value is an array of (parentclass, childclass). If $component is null, then all many-many
2115 * components are returned.
2116 *
2117 * @see DataObject::manyManyComponent()
2118 * @param string $component Deprecated - Name of component
2119 * @return array|null An array of (parentclass, childclass), or an array of all many-many components
2120 */
2121 public function manyMany($component = null) {
2122 if($component) {
2123 Deprecation::notice(
2124 '4.0',
2125 'Please use DataObject::manyManyComponent() instead of passing a component name to manyMany()',
2126 Deprecation::SCOPE_GLOBAL
2127 );
2128 return $this->manyManyComponent($component);
2129 }
2130
2131 $manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED);
2132 $belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2133
2134 $items = array_merge($manyManys, $belongsManyManys);
2135 return $items;
2136 }
2137
2138 /**
2139 * Return information about a specific many_many component. Returns a numeric array of:
2140 * array(
2141 * <classname>, The class that relation is defined in e.g. "Product"
2142 * <candidateName>, The target class of the relation e.g. "Category"
2143 * <parentField>, The field name pointing to <classname>'s table e.g. "ProductID"
2144 * <childField>, The field name pointing to <candidatename>'s table e.g. "CategoryID"
2145 * <joinTable> The join table between the two classes e.g. "Product_Categories"
2146 * )
2147 * @param string $component The component name
2148 * @return array|null
2149 */
2150 public function manyManyComponent($component) {
2151 $classes = $this->getClassAncestry();
2152 foreach($classes as $class) {
2153 $manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
2154 // Check if the component is defined in many_many on this class
2155 $candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2156 if($candidate) {
2157 $parentField = $class . "ID";
2158 $childField = ($class == $candidate) ? "ChildID" : $candidate . "ID";
2159 return array($class, $candidate, $parentField, $childField, "{$class}_$component");
2160 }
2161
2162 // Check if the component is defined in belongs_many_many on this class
2163 $belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
2164 $candidate = (isset($belongsManyMany[$component])) ? $belongsManyMany[$component] : null;
2165 if($candidate) {
2166 // Extract class and relation name from dot-notation
2167 if(strpos($candidate, '.') !== false) {
2168 list($candidate, $relationName) = explode('.', $candidate, 2);
2169 }
2170
2171 $childField = $candidate . "ID";
2172
2173 // We need to find the inverse component name
2174 $otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2175 if(!$otherManyMany) {
2176 throw new LogicException("Inverse component of $candidate not found ({$this->class})");
2177 }
2178
2179 // If we've got a relation name (extracted from dot-notation), we can already work out
2180 // the join table and candidate class name...
2181 if(isset($relationName) && isset($otherManyMany[$relationName])) {
2182 $candidateClass = $otherManyMany[$relationName];
2183 $joinTable = "{$candidate}_{$relationName}";
2184 } else {
2185 // ... otherwise, we need to loop over the many_manys and find a relation that
2186 // matches up to this class
2187 foreach($otherManyMany as $inverseComponentName => $candidateClass) {
2188 if($candidateClass == $class || is_subclass_of($class, $candidateClass)) {
2189 $joinTable = "{$candidate}_{$inverseComponentName}";
2190 break;
2191 }
2192 }
2193 }
2194
2195 // If we could work out the join table, we've got all the info we need
2196 if(isset($joinTable)) {
2197 $parentField = ($class == $candidate) ? "ChildID" : $candidateClass . "ID";
2198 return array($class, $candidate, $parentField, $childField, $joinTable);
2199 }
2200
2201 throw new LogicException("Orphaned \$belongs_many_many value for $this->class.$component");
2202 }
2203 }
2204 }
2205
2206 /**
2207 * This returns an array (if it exists) describing the database extensions that are required, or false if none
2208 *
2209 * This is experimental, and is currently only a Postgres-specific enhancement.
2210 *
2211 * @return array or false
2212 */
2213 public function database_extensions($class){
2214 $extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
2215
2216 if($extensions)
2217 return $extensions;
2218 else
2219 return false;
2220 }
2221
2222 /**
2223 * Generates a SearchContext to be used for building and processing
2224 * a generic search form for properties on this object.
2225 *
2226 * @return SearchContext
2227 */
2228 public function getDefaultSearchContext() {
2229 return new SearchContext(
2230 $this->class,
2231 $this->scaffoldSearchFields(),
2232 $this->defaultSearchFilters()
2233 );
2234 }
2235
2236 /**
2237 * Determine which properties on the DataObject are
2238 * searchable, and map them to their default {@link FormField}
2239 * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
2240 *
2241 * Some additional logic is included for switching field labels, based on
2242 * how generic or specific the field type is.
2243 *
2244 * Used by {@link SearchContext}.
2245 *
2246 * @param array $_params
2247 * 'fieldClasses': Associative array of field names as keys and FormField classes as values
2248 * 'restrictFields': Numeric array of a field name whitelist
2249 * @return FieldList
2250 */
2251 public function scaffoldSearchFields($_params = null) {
2252 $params = array_merge(
2253 array(
2254 'fieldClasses' => false,
2255 'restrictFields' => false
2256 ),
2257 (array)$_params
2258 );
2259 $fields = new FieldList();
2260 foreach($this->searchableFields() as $fieldName => $spec) {
2261 if($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) continue;
2262
2263 // If a custom fieldclass is provided as a string, use it
2264 if($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2265 $fieldClass = $params['fieldClasses'][$fieldName];
2266 $field = new $fieldClass($fieldName);
2267 // If we explicitly set a field, then construct that
2268 } else if(isset($spec['field'])) {
2269 // If it's a string, use it as a class name and construct
2270 if(is_string($spec['field'])) {
2271 $fieldClass = $spec['field'];
2272 $field = new $fieldClass($fieldName);
2273
2274 // If it's a FormField object, then just use that object directly.
2275 } else if($spec['field'] instanceof FormField) {
2276 $field = $spec['field'];
2277
2278 // Otherwise we have a bug
2279 } else {
2280 user_error("Bad value for searchable_fields, 'field' value: "
2281 . var_export($spec['field'], true), E_USER_WARNING);
2282 }
2283
2284 // Otherwise, use the database field's scaffolder
2285 } else {
2286 $field = $this->relObject($fieldName)->scaffoldSearchField();
2287 }
2288
2289 if (strstr($fieldName, '.')) {
2290 $field->setName(str_replace('.', '__', $fieldName));
2291 }
2292 $field->setTitle($spec['title']);
2293
2294 $fields->push($field);
2295 }
2296 return $fields;
2297 }
2298
2299 /**
2300 * Scaffold a simple edit form for all properties on this dataobject,
2301 * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2302 * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2303 *
2304 * @uses FormScaffolder
2305 *
2306 * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2307 * @return FieldList
2308 */
2309 public function scaffoldFormFields($_params = null) {
2310 $params = array_merge(
2311 array(
2312 'tabbed' => false,
2313 'includeRelations' => false,
2314 'restrictFields' => false,
2315 'fieldClasses' => false,
2316 'ajaxSafe' => false
2317 ),
2318 (array)$_params
2319 );
2320
2321 $fs = new FormScaffolder($this);
2322 $fs->tabbed = $params['tabbed'];
2323 $fs->includeRelations = $params['includeRelations'];
2324 $fs->restrictFields = $params['restrictFields'];
2325 $fs->fieldClasses = $params['fieldClasses'];
2326 $fs->ajaxSafe = $params['ajaxSafe'];
2327
2328 return $fs->getFieldList();
2329 }
2330
2331 /**
2332 * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2333 * being called on extensions
2334 *
2335 * @param callable $callback The callback to execute
2336 */
2337 protected function beforeUpdateCMSFields($callback) {
2338 $this->beforeExtending('updateCMSFields', $callback);
2339 }
2340
2341 /**
2342 * Centerpiece of every data administration interface in Silverstripe,
2343 * which returns a {@link FieldList} suitable for a {@link Form} object.
2344 * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2345 * generate this set. To customize, overload this method in a subclass
2346 * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2347 *
2348 * <code>
2349 * class MyCustomClass extends DataObject {
2350 * static $db = array('CustomProperty'=>'Boolean');
2351 *
2352 * function getCMSFields() {
2353 * $fields = parent::getCMSFields();
2354 * $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2355 * return $fields;
2356 * }
2357 * }
2358 * </code>
2359 *
2360 * @see Good example of complex FormField building: SiteTree::getCMSFields()
2361 *
2362 * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2363 */
2364 public function getCMSFields() {
2365 $tabbedFields = $this->scaffoldFormFields(array(
2366 // Don't allow has_many/many_many relationship editing before the record is first saved
2367 'includeRelations' => ($this->ID > 0),
2368 'tabbed' => true,
2369 'ajaxSafe' => true
2370 ));
2371
2372 $this->extend('updateCMSFields', $tabbedFields);
2373
2374 return $tabbedFields;
2375 }
2376
2377 /**
2378 * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2379 * including that dataobject's extensions customised actions could be added to the EditForm.
2380 *
2381 * @return an Empty FieldList(); need to be overload by solid subclass
2382 */
2383 public function getCMSActions() {
2384 $actions = new FieldList();
2385 $this->extend('updateCMSActions', $actions);
2386 return $actions;
2387 }
2388
2389
2390 /**
2391 * Used for simple frontend forms without relation editing
2392 * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2393 * by default. To customize, either overload this method in your
2394 * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2395 *
2396 * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2397 *
2398 * @param array $params See {@link scaffoldFormFields()}
2399 * @return FieldList Always returns a simple field collection without TabSet.
2400 */
2401 public function getFrontEndFields($params = null) {
2402 $untabbedFields = $this->scaffoldFormFields($params);
2403 $this->extend('updateFrontEndFields', $untabbedFields);
2404
2405 return $untabbedFields;
2406 }
2407
2408 /**
2409 * Gets the value of a field.
2410 * Called by {@link __get()} and any getFieldName() methods you might create.
2411 *
2412 * @param string $field The name of the field
2413 *
2414 * @return mixed The field value
2415 */
2416 public function getField($field) {
2417 // If we already have an object in $this->record, then we should just return that
2418 if(isset($this->record[$field]) && is_object($this->record[$field])) return $this->record[$field];
2419
2420 // Do we have a field that needs to be lazy loaded?
2421 if(isset($this->record[$field.'_Lazy'])) {
2422 $tableClass = $this->record[$field.'_Lazy'];
2423 $this->loadLazyFields($tableClass);
2424 }
2425
2426 // Otherwise, we need to determine if this is a complex field
2427 if(self::is_composite_field($this->class, $field)) {
2428 $helper = $this->castingHelper($field);
2429 $fieldObj = Object::create_from_string($helper, $field);
2430
2431 $compositeFields = $fieldObj->compositeDatabaseFields();
2432 foreach ($compositeFields as $compositeName => $compositeType) {
2433 if(isset($this->record[$field.$compositeName.'_Lazy'])) {
2434 $tableClass = $this->record[$field.$compositeName.'_Lazy'];
2435 $this->loadLazyFields($tableClass);
2436 }
2437 }
2438
2439 // write value only if either the field value exists,
2440 // or a valid record has been loaded from the database
2441 $value = (isset($this->record[$field])) ? $this->record[$field] : null;
2442 if($value || $this->exists()) $fieldObj->setValue($value, $this->record, false);
2443
2444 $this->record[$field] = $fieldObj;
2445
2446 return $this->record[$field];
2447 }
2448
2449 return isset($this->record[$field]) ? $this->record[$field] : null;
2450 }
2451
2452 /**
2453 * Loads all the stub fields that an initial lazy load didn't load fully.
2454 *
2455 * @param string $tableClass Base table to load the values from. Others are joined as required.
2456 * Not specifying a tableClass will load all lazy fields from all tables.
2457 * @return bool Flag if lazy loading succeeded
2458 */
2459 protected function loadLazyFields($tableClass = null) {
2460 if(!$this->isInDB() || !is_numeric($this->ID)) {
2461 return false;
2462 }
2463
2464 if (!$tableClass) {
2465 $loaded = array();
2466
2467 foreach ($this->record as $key => $value) {
2468 if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2469 $this->loadLazyFields($value);
2470 $loaded[$value] = $value;
2471 }
2472 }
2473
2474 return false;
2475 }
2476
2477 $dataQuery = new DataQuery($tableClass);
2478
2479 // Reset query parameter context to that of this DataObject
2480 if($params = $this->getSourceQueryParams()) {
2481 foreach($params as $key => $value) $dataQuery->setQueryParam($key, $value);
2482 }
2483
2484 // Limit query to the current record, unless it has the Versioned extension,
2485 // in which case it requires special handling through augmentLoadLazyFields()
2486 if(!$this->hasExtension('Versioned')) {
2487 $dataQuery->where("\"$tableClass\".\"ID\" = {$this->record['ID']}")->limit(1);
2488 }
2489
2490 $columns = array();
2491
2492 // Add SQL for fields, both simple & multi-value
2493 // TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2494 $databaseFields = self::database_fields($tableClass, false);
2495 if($databaseFields) foreach($databaseFields as $k => $v) {
2496 if(!isset($this->record[$k]) || $this->record[$k] === null) {
2497 $columns[] = $k;
2498 }
2499 }
2500
2501 if ($columns) {
2502 $query = $dataQuery->query();
2503 $this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2504 $this->extend('augmentSQL', $query, $dataQuery);
2505
2506 $dataQuery->setQueriedColumns($columns);
2507 $newData = $dataQuery->execute()->record();
2508
2509 // Load the data into record
2510 if($newData) {
2511 foreach($newData as $k => $v) {
2512 if (in_array($k, $columns)) {
2513 $this->record[$k] = $v;
2514 $this->original[$k] = $v;
2515 unset($this->record[$k . '_Lazy']);
2516 }
2517 }
2518
2519 // No data means that the query returned nothing; assign 'null' to all the requested fields
2520 } else {
2521 foreach($columns as $k) {
2522 $this->record[$k] = null;
2523 $this->original[$k] = null;
2524 unset($this->record[$k . '_Lazy']);
2525 }
2526 }
2527 }
2528 return true;
2529 }
2530
2531 /**
2532 * Return the fields that have changed.
2533 *
2534 * The change level affects what the functions defines as "changed":
2535 * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2536 * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2537 * for example a change from 0 to null would not be included.
2538 *
2539 * Example return:
2540 * <code>
2541 * array(
2542 * 'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2543 * )
2544 * </code>
2545 *
2546 * @param boolean $databaseFieldsOnly Get only database fields that have changed
2547 * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2548 * @return array
2549 */
2550 public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT) {
2551 $changedFields = array();
2552
2553 // Update the changed array with references to changed obj-fields
2554 foreach($this->record as $k => $v) {
2555 if(is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2556 $this->changed[$k] = self::CHANGE_VALUE;
2557 }
2558 }
2559
2560 if($databaseFieldsOnly) {
2561 // Merge all DB fields together
2562 $inheritedFields = $this->inheritedDatabaseFields();
2563 $compositeFields = static::composite_fields(get_class($this));
2564 $fixedFields = $this->config()->fixed_fields;
2565 $databaseFields = array_merge(
2566 $inheritedFields,
2567 $fixedFields,
2568 $compositeFields
2569 );
2570 $fields = array_intersect_key((array)$this->changed, $databaseFields);
2571 } else {
2572 $fields = $this->changed;
2573 }
2574
2575 // Filter the list to those of a certain change level
2576 if($changeLevel > self::CHANGE_STRICT) {
2577 if($fields) foreach($fields as $name => $level) {
2578 if($level < $changeLevel) {
2579 unset($fields[$name]);
2580 }
2581 }
2582 }
2583
2584 if($fields) foreach($fields as $name => $level) {
2585 $changedFields[$name] = array(
2586 'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2587 'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2588 'level' => $level
2589 );
2590 }
2591
2592 return $changedFields;
2593 }
2594
2595 /**
2596 * Uses {@link getChangedFields()} to determine if fields have been changed
2597 * since loading them from the database.
2598 *
2599 * @param string $fieldName Name of the database field to check, will check for any if not given
2600 * @param int $changeLevel See {@link getChangedFields()}
2601 * @return boolean
2602 */
2603 public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT) {
2604 if (!$fieldName) {
2605 // Limit "any changes" to db fields only
2606 $changed = $this->getChangedFields(true, $changeLevel);
2607 return !empty($changed);
2608 } else {
2609 // Given a field name, check all fields
2610 $changed = $this->getChangedFields(false, $changeLevel);
2611 return array_key_exists($fieldName, $changed);
2612 }
2613 }
2614
2615 /**
2616 * Set the value of the field
2617 * Called by {@link __set()} and any setFieldName() methods you might create.
2618 *
2619 * @param string $fieldName Name of the field
2620 * @param mixed $val New field value
2621 * @return DataObject $this
2622 */
2623 public function setField($fieldName, $val) {
2624 //if it's a has_one component, destroy the cache
2625 if (substr($fieldName, -2) == 'ID') {
2626 unset($this->components[substr($fieldName, 0, -2)]);
2627 }
2628 // Situation 1: Passing an DBField
2629 if($val instanceof DBField) {
2630 $val->Name = $fieldName;
2631
2632 // If we've just lazy-loaded the column, then we need to populate the $original array by
2633 // called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
2634 // on a call to getChanged()?
2635 $this->getField($fieldName);
2636
2637 $this->record[$fieldName] = $val;
2638 // Situation 2: Passing a literal or non-DBField object
2639 } else {
2640 // If this is a proper database field, we shouldn't be getting non-DBField objects
2641 if(is_object($val) && $this->db($fieldName)) {
2642 user_error('DataObject::setField: passed an object that is not a DBField', E_USER_WARNING);
2643 }
2644
2645 // if a field is not existing or has strictly changed
2646 if(!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2647 // TODO Add check for php-level defaults which are not set in the db
2648 // TODO Add check for hidden input-fields (readonly) which are not set in the db
2649 // At the very least, the type has changed
2650 $this->changed[$fieldName] = self::CHANGE_STRICT;
2651
2652 if((!isset($this->record[$fieldName]) && $val) || (isset($this->record[$fieldName])
2653 && $this->record[$fieldName] != $val)) {
2654
2655 // Value has changed as well, not just the type
2656 $this->changed[$fieldName] = self::CHANGE_VALUE;
2657 }
2658
2659 // If we've just lazy-loaded the column, then we need to populate the $original array by
2660 // called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
2661 // on a call to getChanged()?
2662 $this->getField($fieldName);
2663
2664 // Value is always saved back when strict check succeeds.
2665 $this->record[$fieldName] = $val;
2666 }
2667 }
2668 return $this;
2669 }
2670
2671 /**
2672 * Set the value of the field, using a casting object.
2673 * This is useful when you aren't sure that a date is in SQL format, for example.
2674 * setCastedField() can also be used, by forms, to set related data. For example, uploaded images
2675 * can be saved into the Image table.
2676 *
2677 * @param string $fieldName Name of the field
2678 * @param mixed $value New field value
2679 * @return DataObject $this
2680 */
2681 public function setCastedField($fieldName, $val) {
2682 if(!$fieldName) {
2683 user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2684 }
2685 $castingHelper = $this->castingHelper($fieldName);
2686 if($castingHelper) {
2687 $fieldObj = Object::create_from_string($castingHelper, $fieldName);
2688 $fieldObj->setValue($val);
2689 $fieldObj->saveInto($this);
2690 } else {
2691 $this->$fieldName = $val;
2692 }
2693 return $this;
2694 }
2695
2696 /**
2697 * {@inheritdoc}
2698 */
2699 public function castingHelper($field) {
2700 if ($fieldSpec = $this->db($field)) {
2701 return $fieldSpec;
2702 }
2703
2704 // many_many_extraFields aren't presented by db(), so we check if the source query params
2705 // provide us with meta-data for a many_many relation we can inspect for extra fields.
2706 $queryParams = $this->getSourceQueryParams();
2707 if (!empty($queryParams['Component.ExtraFields'])) {
2708 $extraFields = $queryParams['Component.ExtraFields'];
2709
2710 if (isset($extraFields[$field])) {
2711 return $extraFields[$field];
2712 }
2713 }
2714
2715 return parent::castingHelper($field);
2716 }
2717
2718 /**
2719 * Returns true if the given field exists in a database column on any of
2720 * the objects tables and optionally look up a dynamic getter with
2721 * get<fieldName>().
2722 *
2723 * @param string $field Name of the field
2724 * @return boolean True if the given field exists
2725 */
2726 public function hasField($field) {
2727 return (
2728 array_key_exists($field, $this->record)
2729 || $this->db($field)
2730 || (substr($field,-2) == 'ID') && $this->hasOneComponent(substr($field,0, -2))
2731 || $this->hasMethod("get{$field}")
2732 );
2733 }
2734
2735 /**
2736 * Returns true if the given field exists as a database column
2737 *
2738 * @param string $field Name of the field
2739 *
2740 * @return boolean
2741 */
2742 public function hasDatabaseField($field) {
2743 if(isset(self::$fixed_fields[$field])) return true;
2744
2745 return array_key_exists($field, $this->inheritedDatabaseFields());
2746 }
2747
2748 /**
2749 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2750 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2751 *
2752 * @param string $field Name of the field
2753 * @return string The field type of the given field
2754 */
2755 public function hasOwnTableDatabaseField($field) {
2756 return self::has_own_table_database_field($this->class, $field);
2757 }
2758
2759 /**
2760 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2761 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2762 *
2763 * @param string $class Class name to check
2764 * @param string $field Name of the field
2765 * @return string The field type of the given field
2766 */
2767 public static function has_own_table_database_field($class, $field) {
2768 // Since database_fields omits 'ID'
2769 if($field == "ID") return "Int";
2770
2771 $fieldMap = self::database_fields($class, false);
2772
2773 // Remove string-based "constructor-arguments" from the DBField definition
2774 if(isset($fieldMap[$field])) {
2775 $spec = $fieldMap[$field];
2776 if(is_string($spec)) return strtok($spec,'(');
2777 else return $spec['type'];
2778 }
2779 }
2780
2781 /**
2782 * Returns true if given class has its own table. Uses the rules for whether the table should exist rather than
2783 * actually looking in the database.
2784 *
2785 * @param string $dataClass
2786 * @return bool
2787 */
2788 public static function has_own_table($dataClass) {
2789 if(!is_subclass_of($dataClass,'DataObject')) return false;
2790
2791 $dataClass = ClassInfo::class_name($dataClass);
2792 if(!isset(DataObject::$cache_has_own_table[$dataClass])) {
2793 if(get_parent_class($dataClass) == 'DataObject') {
2794 DataObject::$cache_has_own_table[$dataClass] = true;
2795 } else {
2796 DataObject::$cache_has_own_table[$dataClass]
2797 = Config::inst()->get($dataClass, 'db', Config::UNINHERITED)
2798 || Config::inst()->get($dataClass, 'has_one', Config::UNINHERITED);
2799 }
2800 }
2801 return DataObject::$cache_has_own_table[$dataClass];
2802 }
2803
2804 /**
2805 * Returns true if the member is allowed to do the given action.
2806 * See {@link extendedCan()} for a more versatile tri-state permission control.
2807 *
2808 * @param string $perm The permission to be checked, such as 'View'.
2809 * @param Member $member The member whose permissions need checking. Defaults to the currently logged
2810 * in user.
2811 *
2812 * @return boolean True if the the member is allowed to do the given action
2813 */
2814 public function can($perm, $member = null) {
2815 if(!isset($member)) {
2816 $member = Member::currentUser();
2817 }
2818 if(Permission::checkMember($member, "ADMIN")) return true;
2819
2820 if($this->manyManyComponent('Can' . $perm)) {
2821 if($this->ParentID && $this->SecurityType == 'Inherit') {
2822 if(!($p = $this->Parent)) {
2823 return false;
2824 }
2825 return $this->Parent->can($perm, $member);
2826
2827 } else {
2828 $permissionCache = $this->uninherited('permissionCache');
2829 $memberID = $member ? $member->ID : 'none';
2830
2831 if(!isset($permissionCache[$memberID][$perm])) {
2832 if($member->ID) {
2833 $groups = $member->Groups();
2834 }
2835
2836 $groupList = implode(', ', $groups->column("ID"));
2837
2838 // TODO Fix relation table hardcoding
2839 $query = new SQLQuery(
2840 "\"Page_Can$perm\".PageID",
2841 array("\"Page_Can$perm\""),
2842 "GroupID IN ($groupList)");
2843
2844 $permissionCache[$memberID][$perm] = $query->execute()->column();
2845
2846 if($perm == "View") {
2847 // TODO Fix relation table hardcoding
2848 $query = new SQLQuery("\"SiteTree\".\"ID\"", array(
2849 "\"SiteTree\"",
2850 "LEFT JOIN \"Page_CanView\" ON \"Page_CanView\".\"PageID\" = \"SiteTree\".\"ID\""
2851 ), "\"Page_CanView\".\"PageID\" IS NULL");
2852
2853 $unsecuredPages = $query->execute()->column();
2854 if($permissionCache[$memberID][$perm]) {
2855 $permissionCache[$memberID][$perm]
2856 = array_merge($permissionCache[$memberID][$perm], $unsecuredPages);
2857 } else {
2858 $permissionCache[$memberID][$perm] = $unsecuredPages;
2859 }
2860 }
2861
2862 Config::inst()->update($this->class, 'permissionCache', $permissionCache);
2863 }
2864
2865 if($permissionCache[$memberID][$perm]) {
2866 return in_array($this->ID, $permissionCache[$memberID][$perm]);
2867 }
2868 }
2869 } else {
2870 return parent::can($perm, $member);
2871 }
2872 }
2873
2874 /**
2875 * Process tri-state responses from permission-alterting extensions. The extensions are
2876 * expected to return one of three values:
2877 *
2878 * - false: Disallow this permission, regardless of what other extensions say
2879 * - true: Allow this permission, as long as no other extensions return false
2880 * - NULL: Don't affect the outcome
2881 *
2882 * This method itself returns a tri-state value, and is designed to be used like this:
2883 *
2884 * <code>
2885 * $extended = $this->extendedCan('canDoSomething', $member);
2886 * if($extended !== null) return $extended;
2887 * else return $normalValue;
2888 * </code>
2889 *
2890 * @param String $methodName Method on the same object, e.g. {@link canEdit()}
2891 * @param Member|int $member
2892 * @return boolean|null
2893 */
2894 public function extendedCan($methodName, $member) {
2895 $results = $this->extend($methodName, $member);
2896 if($results && is_array($results)) {
2897 // Remove NULLs
2898 $results = array_filter($results, function($v) {return !is_null($v);});
2899 // If there are any non-NULL responses, then return the lowest one of them.
2900 // If any explicitly deny the permission, then we don't get access
2901 if($results) return min($results);
2902 }
2903 return null;
2904 }
2905
2906 /**
2907 * @param Member $member
2908 * @return boolean
2909 */
2910 public function canView($member = null) {
2911 $extended = $this->extendedCan(__FUNCTION__, $member);
2912 if($extended !== null) {
2913 return $extended;
2914 }
2915 return Permission::check('ADMIN', 'any', $member);
2916 }
2917
2918 /**
2919 * @param Member $member
2920 * @return boolean
2921 */
2922 public function canEdit($member = null) {
2923 $extended = $this->extendedCan(__FUNCTION__, $member);
2924 if($extended !== null) {
2925 return $extended;
2926 }
2927 return Permission::check('ADMIN', 'any', $member);
2928 }
2929
2930 /**
2931 * @param Member $member
2932 * @return boolean
2933 */
2934 public function canDelete($member = null) {
2935 $extended = $this->extendedCan(__FUNCTION__, $member);
2936 if($extended !== null) {
2937 return $extended;
2938 }
2939 return Permission::check('ADMIN', 'any', $member);
2940 }
2941
2942 /**
2943 * @todo Should canCreate be a static method?
2944 *
2945 * @param Member $member
2946 * @return boolean
2947 */
2948 public function canCreate($member = null) {
2949 $extended = $this->extendedCan(__FUNCTION__, $member);
2950 if($extended !== null) {
2951 return $extended;
2952 }
2953 return Permission::check('ADMIN', 'any', $member);
2954 }
2955
2956 /**
2957 * Debugging used by Debug::show()
2958 *
2959 * @return string HTML data representing this object
2960 */
2961 public function debug() {
2962 $val = "<h3>Database record: $this->class</h3>\n<ul>\n";
2963 if($this->record) foreach($this->record as $fieldName => $fieldVal) {
2964 $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2965 }
2966 $val .= "</ul>\n";
2967 return $val;
2968 }
2969
2970 /**
2971 * Return the DBField object that represents the given field.
2972 * This works similarly to obj() with 2 key differences:
2973 * - it still returns an object even when the field has no value.
2974 * - it only matches fields and not methods
2975 * - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2976 *
2977 * @param string $fieldName Name of the field
2978 * @return DBField The field as a DBField object
2979 */
2980 public function dbObject($fieldName) {
2981 // If we have a CompositeDBField object in $this->record, then return that
2982 if(isset($this->record[$fieldName]) && is_object($this->record[$fieldName])) {
2983 return $this->record[$fieldName];
2984
2985 // Special case for ID field
2986 } else if($fieldName == 'ID') {
2987 return new PrimaryKey($fieldName, $this);
2988
2989 // Special case for ClassName
2990 } else if($fieldName == 'ClassName') {
2991 $val = get_class($this);
2992 return DBField::create_field('Varchar', $val, $fieldName);
2993
2994 } else if(array_key_exists($fieldName, self::$fixed_fields)) {
2995 return DBField::create_field(self::$fixed_fields[$fieldName], $this->$fieldName, $fieldName);
2996
2997 // General casting information for items in $db
2998 } else if($helper = $this->db($fieldName)) {
2999 $obj = Object::create_from_string($helper, $fieldName);
3000 $obj->setValue($this->$fieldName, $this->record, false);
3001 return $obj;
3002
3003 // Special case for has_one relationships
3004 } else if(preg_match('/ID$/', $fieldName) && $this->hasOneComponent(substr($fieldName,0,-2))) {
3005 $val = $this->$fieldName;
3006 return DBField::create_field('ForeignKey', $val, $fieldName, $this);
3007
3008 // has_one for polymorphic relations do not end in ID
3009 } else if(($type = $this->hasOneComponent($fieldName)) && ($type === 'DataObject')) {
3010 $val = $this->$fieldName();
3011 return DBField::create_field('PolymorphicForeignKey', $val, $fieldName, $this);
3012
3013 }
3014 }
3015
3016 /**
3017 * Traverses to a DBField referenced by relationships between data objects.
3018 *
3019 * The path to the related field is specified with dot separated syntax
3020 * (eg: Parent.Child.Child.FieldName).
3021 *
3022 * @param string $fieldPath
3023 *
3024 * @return mixed DBField of the field on the object or a DataList instance.
3025 */
3026 public function relObject($fieldPath) {
3027 $object = null;
3028
3029 if(strpos($fieldPath, '.') !== false) {
3030 $parts = explode('.', $fieldPath);
3031 $fieldName = array_pop($parts);
3032
3033 // Traverse dot syntax
3034 $component = $this;
3035
3036 foreach($parts as $relation) {
3037 if($component instanceof SS_List) {
3038 if(method_exists($component,$relation)) {
3039 $component = $component->$relation();
3040 } else {
3041 $component = $component->relation($relation);
3042 }
3043 } else {
3044 $component = $component->$relation();
3045 }
3046 }
3047
3048 $object = $component->dbObject($fieldName);
3049
3050 } else {
3051 $object = $this->dbObject($fieldPath);
3052 }
3053
3054 return $object;
3055 }
3056
3057 /**
3058 * Traverses to a field referenced by relationships between data objects, returning the value
3059 * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
3060 *
3061 * @param $fieldPath string
3062 * @return string | null - will return null on a missing value
3063 */
3064 public function relField($fieldName) {
3065 $component = $this;
3066
3067 // We're dealing with relations here so we traverse the dot syntax
3068 if(strpos($fieldName, '.') !== false) {
3069 $relations = explode('.', $fieldName);
3070 $fieldName = array_pop($relations);
3071 foreach($relations as $relation) {
3072 // Inspect $component for element $relation
3073 if($component->hasMethod($relation)) {
3074 // Check nested method
3075 $component = $component->$relation();
3076 } elseif($component instanceof SS_List) {
3077 // Select adjacent relation from DataList
3078 $component = $component->relation($relation);
3079 } elseif($component instanceof DataObject
3080 && ($dbObject = $component->dbObject($relation))
3081 ) {
3082 // Select db object
3083 $component = $dbObject;
3084 } else {
3085 user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
3086 }
3087 }
3088 }
3089
3090 // Bail if the component is null
3091 if(!$component) {
3092 return null;
3093 }
3094 if($component->hasMethod($fieldName)) {
3095 return $component->$fieldName();
3096 }
3097 return $component->$fieldName;
3098 }
3099
3100 /**
3101 * Temporary hack to return an association name, based on class, to get around the mangle
3102 * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
3103 *
3104 * @return String
3105 */
3106 public function getReverseAssociation($className) {
3107 if (is_array($this->manyMany())) {
3108 $many_many = array_flip($this->manyMany());
3109 if (array_key_exists($className, $many_many)) return $many_many[$className];
3110 }
3111 if (is_array($this->hasMany())) {
3112 $has_many = array_flip($this->hasMany());
3113 if (array_key_exists($className, $has_many)) return $has_many[$className];
3114 }
3115 if (is_array($this->hasOne())) {
3116 $has_one = array_flip($this->hasOne());
3117 if (array_key_exists($className, $has_one)) return $has_one[$className];
3118 }
3119
3120 return false;
3121 }
3122
3123 /**
3124 * Return all objects matching the filter
3125 * sub-classes are automatically selected and included
3126 *
3127 * @param string $callerClass The class of objects to be returned
3128 * @param string|array $filter A filter to be inserted into the WHERE clause.
3129 * Supports parameterised queries. See SQLQuery::addWhere() for syntax examples.
3130 * @param string|array $sort A sort expression to be inserted into the ORDER
3131 * BY clause. If omitted, self::$default_sort will be used.
3132 * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
3133 * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
3134 * @param string $containerClass The container class to return the results in.
3135 *
3136 * @todo $containerClass is Ignored, why?
3137 *
3138 * @return DataList The objects matching the filter, in the class specified by $containerClass
3139 */
3140 public static function get($callerClass = null, $filter = "", $sort = "", $join = "", $limit = null,
3141 $containerClass = 'DataList') {
3142
3143 if($callerClass == null) {
3144 $callerClass = get_called_class();
3145 if($callerClass == 'DataObject') {
3146 throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
3147 }
3148
3149 if($filter || $sort || $join || $limit || ($containerClass != 'DataList')) {
3150 throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
3151 . ' arguments');
3152 }
3153
3154 $result = DataList::create(get_called_class());
3155 $result->setDataModel(DataModel::inst());
3156 return $result;
3157 }
3158
3159 if($join) {
3160 throw new \InvalidArgumentException(
3161 'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
3162 );
3163 }
3164
3165 $result = DataList::create($callerClass)->where($filter)->sort($sort);
3166
3167 if($limit && strpos($limit, ',') !== false) {
3168 $limitArguments = explode(',', $limit);
3169 $result = $result->limit($limitArguments[1],$limitArguments[0]);
3170 } elseif($limit) {
3171 $result = $result->limit($limit);
3172 }
3173
3174 $result->setDataModel(DataModel::inst());
3175 return $result;
3176 }
3177
3178
3179 /**
3180 * @deprecated
3181 */
3182 public function Aggregate($class = null) {
3183 Deprecation::notice('4.0', 'Call aggregate methods on a DataList directly instead. In templates'
3184 . ' an example of the new syntax is <% cached List(Member).max(LastEdited) %> instead'
3185 . ' (check partial-caching.md documentation for more details.)');
3186
3187 if($class) {
3188 $list = new DataList($class);
3189 $list->setDataModel(DataModel::inst());
3190 } else if(isset($this)) {
3191 $list = new DataList(get_class($this));
3192 $list->setDataModel($this->model);
3193 } else {
3194 throw new \InvalidArgumentException("DataObject::aggregate() must be called as an instance method or passed"
3195 . " a classname");
3196 }
3197 return $list;
3198 }
3199
3200 /**
3201 * @deprecated
3202 */
3203 public function RelationshipAggregate($relationship) {
3204 Deprecation::notice('4.0', 'Call aggregate methods on a relationship directly instead.');
3205
3206 return $this->$relationship();
3207 }
3208
3209 /**
3210 * Return the first item matching the given query.
3211 * All calls to get_one() are cached.
3212 *
3213 * @param string $callerClass The class of objects to be returned
3214 * @param string|array $filter A filter to be inserted into the WHERE clause.
3215 * Supports parameterised queries. See SQLQuery::addWhere() for syntax examples.
3216 * @param boolean $cache Use caching
3217 * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
3218 *
3219 * @return DataObject The first item matching the query
3220 */
3221 public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "") {
3222 $SNG = singleton($callerClass);
3223
3224 $cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
3225 $cacheKey = md5(var_export($cacheComponents, true));
3226
3227 // Flush destroyed items out of the cache
3228 if($cache && isset(DataObject::$_cache_get_one[$callerClass][$cacheKey])
3229 && DataObject::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject
3230 && DataObject::$_cache_get_one[$callerClass][$cacheKey]->destroyed) {
3231
3232 DataObject::$_cache_get_one[$callerClass][$cacheKey] = false;
3233 }
3234 if(!$cache || !isset(DataObject::$_cache_get_one[$callerClass][$cacheKey])) {
3235 $dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
3236 $item = $dl->First();
3237
3238 if($cache) {
3239 DataObject::$_cache_get_one[$callerClass][$cacheKey] = $item;
3240 if(!DataObject::$_cache_get_one[$callerClass][$cacheKey]) {
3241 DataObject::$_cache_get_one[$callerClass][$cacheKey] = false;
3242 }
3243 }
3244 }
3245 return $cache ? DataObject::$_cache_get_one[$callerClass][$cacheKey] : $item;
3246 }
3247
3248 /**
3249 * Flush the cached results for all relations (has_one, has_many, many_many)
3250 * Also clears any cached aggregate data.
3251 *
3252 * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
3253 * When false will just clear session-local cached data
3254 * @return DataObject $this
3255 */
3256 public function flushCache($persistent = true) {
3257 if($persistent) Aggregate::flushCache($this->class);
3258
3259 if($this->class == 'DataObject') {
3260 DataObject::$_cache_get_one = array();
3261 return $this;
3262 }
3263
3264 $classes = ClassInfo::ancestry($this->class);
3265 foreach($classes as $class) {
3266 if(isset(DataObject::$_cache_get_one[$class])) unset(DataObject::$_cache_get_one[$class]);
3267 }
3268
3269 $this->extend('flushCache');
3270
3271 $this->components = array();
3272 return $this;
3273 }
3274
3275 /**
3276 * Flush the get_one global cache and destroy associated objects.
3277 */
3278 public static function flush_and_destroy_cache() {
3279 if(DataObject::$_cache_get_one) foreach(DataObject::$_cache_get_one as $class => $items) {
3280 if(is_array($items)) foreach($items as $item) {
3281 if($item) $item->destroy();
3282 }
3283 }
3284 DataObject::$_cache_get_one = array();
3285 }
3286
3287 /**
3288 * Reset all global caches associated with DataObject.
3289 */
3290 public static function reset() {
3291 self::clear_classname_spec_cache();
3292 DataObject::$cache_has_own_table = array();
3293 DataObject::$_cache_db = array();
3294 DataObject::$_cache_get_one = array();
3295 DataObject::$_cache_composite_fields = array();
3296 DataObject::$_cache_is_composite_field = array();
3297 DataObject::$_cache_custom_database_fields = array();
3298 DataObject::$_cache_get_class_ancestry = array();
3299 DataObject::$_cache_field_labels = array();
3300 }
3301
3302 /**
3303 * Return the given element, searching by ID
3304 *
3305 * @param string $callerClass The class of the object to be returned
3306 * @param int $id The id of the element
3307 * @param boolean $cache See {@link get_one()}
3308 *
3309 * @return DataObject The element
3310 */
3311 public static function get_by_id($callerClass, $id, $cache = true) {
3312 if(!is_numeric($id)) {
3313 user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
3314 }
3315
3316 // Check filter column
3317 if(is_subclass_of($callerClass, 'DataObject')) {
3318 $baseClass = ClassInfo::baseDataClass($callerClass);
3319 $column = "\"$baseClass\".\"ID\"";
3320 } else{
3321 // This simpler code will be used by non-DataObject classes that implement DataObjectInterface
3322 $column = '"ID"';
3323 }
3324
3325 // Relegate to get_one
3326 return DataObject::get_one($callerClass, array($column => $id), $cache);
3327 }
3328
3329 /**
3330 * Get the name of the base table for this object
3331 */
3332 public function baseTable() {
3333 $tableClasses = ClassInfo::dataClassesFor($this->class);
3334 return array_shift($tableClasses);
3335 }
3336
3337 /**
3338 * @var Array Parameters used in the query that built this object.
3339 * This can be used by decorators (e.g. lazy loading) to
3340 * run additional queries using the same context.
3341 */
3342 protected $sourceQueryParams;
3343
3344 /**
3345 * @see $sourceQueryParams
3346 * @return array
3347 */
3348 public function getSourceQueryParams() {
3349 return $this->sourceQueryParams;
3350 }
3351
3352 /**
3353 * @see $sourceQueryParams
3354 * @param array
3355 */
3356 public function setSourceQueryParams($array) {
3357 $this->sourceQueryParams = $array;
3358 }
3359
3360 /**
3361 * @see $sourceQueryParams
3362 * @param array
3363 */
3364 public function setSourceQueryParam($key, $value) {
3365 $this->sourceQueryParams[$key] = $value;
3366 }
3367
3368 /**
3369 * @see $sourceQueryParams
3370 * @return Mixed
3371 */
3372 public function getSourceQueryParam($key) {
3373 if(isset($this->sourceQueryParams[$key])) return $this->sourceQueryParams[$key];
3374 else return null;
3375 }
3376
3377 //-------------------------------------------------------------------------------------------//
3378
3379 /**
3380 * Return the database indexes on this table.
3381 * This array is indexed by the name of the field with the index, and
3382 * the value is the type of index.
3383 */
3384 public function databaseIndexes() {
3385 $has_one = $this->uninherited('has_one',true);
3386 $classIndexes = $this->uninherited('indexes',true);
3387 //$fileIndexes = $this->uninherited('fileIndexes', true);
3388
3389 $indexes = array();
3390
3391 if($has_one) {
3392 foreach($has_one as $relationshipName => $fieldType) {
3393 $indexes[$relationshipName . 'ID'] = true;
3394 }
3395 }
3396
3397 if($classIndexes) {
3398 foreach($classIndexes as $indexName => $indexType) {
3399 $indexes[$indexName] = $indexType;
3400 }
3401 }
3402
3403 if(get_parent_class($this) == "DataObject") {
3404 $indexes['ClassName'] = true;
3405 }
3406
3407 return $indexes;
3408 }
3409
3410 /**
3411 * Check the database schema and update it as necessary.
3412 *
3413 * @uses DataExtension->augmentDatabase()
3414 */
3415 public function requireTable() {
3416 // Only build the table if we've actually got fields
3417 $fields = self::database_fields($this->class);
3418 $extensions = self::database_extensions($this->class);
3419
3420 $indexes = $this->databaseIndexes();
3421
3422 // Validate relationship configuration
3423 $this->validateModelDefinitions();
3424
3425 if($fields) {
3426 $hasAutoIncPK = ($this->class == ClassInfo::baseDataClass($this->class));
3427 DB::require_table($this->class, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'),
3428 $extensions);
3429 } else {
3430 DB::dont_require_table($this->class);
3431 }
3432
3433 // Build any child tables for many_many items
3434 if($manyMany = $this->uninherited('many_many', true)) {
3435 $extras = $this->uninherited('many_many_extraFields', true);
3436 foreach($manyMany as $relationship => $childClass) {
3437 // Build field list
3438 $manymanyFields = array(
3439 "{$this->class}ID" => "Int",
3440 (($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => "Int",
3441 );
3442 if(isset($extras[$relationship])) {
3443 $manymanyFields = array_merge($manymanyFields, $extras[$relationship]);
3444 }
3445
3446 // Build index list
3447 $manymanyIndexes = array(
3448 "{$this->class}ID" => true,
3449 (($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => true,
3450 );
3451
3452 DB::require_table("{$this->class}_$relationship", $manymanyFields, $manymanyIndexes, true, null,
3453 $extensions);
3454 }
3455 }
3456
3457 // Let any extentions make their own database fields
3458 $this->extend('augmentDatabase', $dummy);
3459 }
3460
3461 /**
3462 * Validate that the configured relations for this class use the correct syntaxes
3463 * @throws LogicException
3464 */
3465 protected function validateModelDefinitions() {
3466 $modelDefinitions = array(
3467 'db' => Config::inst()->get($this->class, 'db', Config::UNINHERITED),
3468 'has_one' => Config::inst()->get($this->class, 'has_one', Config::UNINHERITED),
3469 'has_many' => Config::inst()->get($this->class, 'has_many', Config::UNINHERITED),
3470 'belongs_to' => Config::inst()->get($this->class, 'belongs_to', Config::UNINHERITED),
3471 'many_many' => Config::inst()->get($this->class, 'many_many', Config::UNINHERITED),
3472 'belongs_many_many' => Config::inst()->get($this->class, 'belongs_many_many', Config::UNINHERITED),
3473 'many_many_extraFields' => Config::inst()->get($this->class, 'many_many_extraFields', Config::UNINHERITED)
3474 );
3475
3476 foreach($modelDefinitions as $defType => $relations) {
3477 if( ! $relations) continue;
3478
3479 foreach($relations as $k => $v) {
3480 if($defType === 'many_many_extraFields') {
3481 if(!is_array($v)) {
3482 throw new LogicException("$this->class::\$many_many_extraFields has a bad entry: "
3483 . var_export($k, true) . " => " . var_export($v, true)
3484 . ". Each many_many_extraFields entry should map to a field specification array.");
3485 }
3486 } else {
3487 if(!is_string($k) || is_numeric($k) || !is_string($v)) {
3488 throw new LogicException("$this->class::$defType has a bad entry: "
3489 . var_export($k, true). " => " . var_export($v, true) . ". Each map key should be a
3490 relationship name, and the map value should be the data class to join to.");
3491 }
3492 }
3493 }
3494 }
3495 }
3496
3497 /**
3498 * Add default records to database. This function is called whenever the
3499 * database is built, after the database tables have all been created. Overload
3500 * this to add default records when the database is built, but make sure you
3501 * call parent::requireDefaultRecords().
3502 *
3503 * @uses DataExtension->requireDefaultRecords()
3504 */
3505 public function requireDefaultRecords() {
3506 $defaultRecords = $this->stat('default_records');
3507
3508 if(!empty($defaultRecords)) {
3509 $hasData = DataObject::get_one($this->class);
3510 if(!$hasData) {
3511 $className = $this->class;
3512 foreach($defaultRecords as $record) {
3513 $obj = $this->model->$className->newObject($record);
3514 $obj->write();
3515 }
3516 DB::alteration_message("Added default records to $className table","created");
3517 }
3518 }
3519
3520 // Let any extentions make their own database default data
3521 $this->extend('requireDefaultRecords', $dummy);
3522 }
3523
3524 /**
3525 * Returns fields bu traversing the class heirachy in a bottom-up direction.
3526 *
3527 * Needed to avoid getCMSFields being empty when customDatabaseFields overlooks
3528 * the inheritance chain of the $db array, where a child data object has no $db array,
3529 * but still needs to know the properties of its parent. This should be merged into databaseFields or
3530 * customDatabaseFields.
3531 *
3532 * @todo review whether this is still needed after recent API changes
3533 */
3534 public function inheritedDatabaseFields() {
3535 $fields = array();
3536 $currentObj = $this->class;
3537
3538 while($currentObj != 'DataObject') {
3539 $fields = array_merge($fields, self::custom_database_fields($currentObj));
3540 $currentObj = get_parent_class($currentObj);
3541 }
3542
3543 return (array) $fields;
3544 }
3545
3546 /**
3547 * Get the default searchable fields for this object, as defined in the
3548 * $searchable_fields list. If searchable fields are not defined on the
3549 * data object, uses a default selection of summary fields.
3550 *
3551 * @return array
3552 */
3553 public function searchableFields() {
3554 // can have mixed format, need to make consistent in most verbose form
3555 $fields = $this->stat('searchable_fields');
3556 $labels = $this->fieldLabels();
3557
3558 // fallback to summary fields (unless empty array is explicitly specified)
3559 if( ! $fields && ! is_array($fields)) {
3560 $summaryFields = array_keys($this->summaryFields());
3561 $fields = array();
3562
3563 // remove the custom getters as the search should not include them
3564 if($summaryFields) {
3565 foreach($summaryFields as $key => $name) {
3566 $spec = $name;
3567
3568 // Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3569 if(($fieldPos = strpos($name, '.')) !== false) {
3570 $name = substr($name, 0, $fieldPos);
3571 }
3572
3573 if($this->hasDatabaseField($name)) {
3574 $fields[] = $name;
3575 } elseif($this->relObject($spec)) {
3576 $fields[] = $spec;
3577 }
3578 }
3579 }
3580 }
3581
3582 // we need to make sure the format is unified before
3583 // augmenting fields, so extensions can apply consistent checks
3584 // but also after augmenting fields, because the extension
3585 // might use the shorthand notation as well
3586
3587 // rewrite array, if it is using shorthand syntax
3588 $rewrite = array();
3589 foreach($fields as $name => $specOrName) {
3590 $identifer = (is_int($name)) ? $specOrName : $name;
3591
3592 if(is_int($name)) {
3593 // Format: array('MyFieldName')
3594 $rewrite[$identifer] = array();
3595 } elseif(is_array($specOrName)) {
3596 // Format: array('MyFieldName' => array(
3597 // 'filter => 'ExactMatchFilter',
3598 // 'field' => 'NumericField', // optional
3599 // 'title' => 'My Title', // optional
3600 // ))
3601 $rewrite[$identifer] = array_merge(
3602 array('filter' => $this->relObject($identifer)->stat('default_search_filter_class')),
3603 (array)$specOrName
3604 );
3605 } else {
3606 // Format: array('MyFieldName' => 'ExactMatchFilter')
3607 $rewrite[$identifer] = array(
3608 'filter' => $specOrName,
3609 );
3610 }
3611 if(!isset($rewrite[$identifer]['title'])) {
3612 $rewrite[$identifer]['title'] = (isset($labels[$identifer]))
3613 ? $labels[$identifer] : FormField::name_to_label($identifer);
3614 }
3615 if(!isset($rewrite[$identifer]['filter'])) {
3616 $rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3617 }
3618 }
3619
3620 $fields = $rewrite;
3621
3622 // apply DataExtensions if present
3623 $this->extend('updateSearchableFields', $fields);
3624
3625 return $fields;
3626 }
3627
3628 /**
3629 * Get any user defined searchable fields labels that
3630 * exist. Allows overriding of default field names in the form
3631 * interface actually presented to the user.
3632 *
3633 * The reason for keeping this separate from searchable_fields,
3634 * which would be a logical place for this functionality, is to
3635 * avoid bloating and complicating the configuration array. Currently
3636 * much of this system is based on sensible defaults, and this property
3637 * would generally only be set in the case of more complex relationships
3638 * between data object being required in the search interface.
3639 *
3640 * Generates labels based on name of the field itself, if no static property
3641 * {@link self::field_labels} exists.
3642 *
3643 * @uses $field_labels
3644 * @uses FormField::name_to_label()
3645 *
3646 * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3647 *
3648 * @return array|string Array of all element labels if no argument given, otherwise the label of the field
3649 */
3650 public function fieldLabels($includerelations = true) {
3651 $cacheKey = $this->class . '_' . $includerelations;
3652
3653 if(!isset(self::$_cache_field_labels[$cacheKey])) {
3654 $customLabels = $this->stat('field_labels');
3655 $autoLabels = array();
3656
3657 // get all translated static properties as defined in i18nCollectStatics()
3658 $ancestry = ClassInfo::ancestry($this->class);
3659 $ancestry = array_reverse($ancestry);
3660 if($ancestry) foreach($ancestry as $ancestorClass) {
3661 if($ancestorClass == 'ViewableData') break;
3662 $types = array(
3663 'db' => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
3664 );
3665 if($includerelations){
3666 $types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
3667 $types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED);
3668 $types['many_many'] = (array)Config::inst()->get($ancestorClass, 'many_many', Config::UNINHERITED);
3669 $types['belongs_many_many'] = (array)Config::inst()->get($ancestorClass, 'belongs_many_many', Config::UNINHERITED);
3670 }
3671 foreach($types as $type => $attrs) {
3672 foreach($attrs as $name => $spec) {
3673 $autoLabels[$name] = _t("{$ancestorClass}.{$type}_{$name}",FormField::name_to_label($name));
3674 }
3675 }
3676 }
3677
3678 $labels = array_merge((array)$autoLabels, (array)$customLabels);
3679 $this->extend('updateFieldLabels', $labels);
3680 self::$_cache_field_labels[$cacheKey] = $labels;
3681 }
3682
3683 return self::$_cache_field_labels[$cacheKey];
3684 }
3685
3686 /**
3687 * Get a human-readable label for a single field,
3688 * see {@link fieldLabels()} for more details.
3689 *
3690 * @uses fieldLabels()
3691 * @uses FormField::name_to_label()
3692 *
3693 * @param string $name Name of the field
3694 * @return string Label of the field
3695 */
3696 public function fieldLabel($name) {
3697 $labels = $this->fieldLabels();
3698 return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
3699 }
3700
3701 /**
3702 * Get the default summary fields for this object.
3703 *
3704 * @todo use the translation apparatus to return a default field selection for the language
3705 *
3706 * @return array
3707 */
3708 public function summaryFields() {
3709 $fields = $this->stat('summary_fields');
3710
3711 // if fields were passed in numeric array,
3712 // convert to an associative array
3713 if($fields && array_key_exists(0, $fields)) {
3714 $fields = array_combine(array_values($fields), array_values($fields));
3715 }
3716
3717 if (!$fields) {
3718 $fields = array();
3719 // try to scaffold a couple of usual suspects
3720 if ($this->hasField('Name')) $fields['Name'] = 'Name';
3721 if ($this->hasDataBaseField('Title')) $fields['Title'] = 'Title';
3722 if ($this->hasField('Description')) $fields['Description'] = 'Description';
3723 if ($this->hasField('FirstName')) $fields['FirstName'] = 'First Name';
3724 }
3725 $this->extend("updateSummaryFields", $fields);
3726
3727 // Final fail-over, just list ID field
3728 if(!$fields) $fields['ID'] = 'ID';
3729
3730 // Localize fields (if possible)
3731 foreach($this->fieldLabels(false) as $name => $label) {
3732 // only attempt to localize if the label definition is the same as the field name.
3733 // this will preserve any custom labels set in the summary_fields configuration
3734 if(isset($fields[$name]) && $name === $fields[$name]) {
3735 $fields[$name] = $label;
3736 }
3737 }
3738
3739 return $fields;
3740 }
3741
3742 /**
3743 * Defines a default list of filters for the search context.
3744 *
3745 * If a filter class mapping is defined on the data object,
3746 * it is constructed here. Otherwise, the default filter specified in
3747 * {@link DBField} is used.
3748 *
3749 * @todo error handling/type checking for valid FormField and SearchFilter subclasses?
3750 *
3751 * @return array
3752 */
3753 public function defaultSearchFilters() {
3754 $filters = array();
3755
3756 foreach($this->searchableFields() as $name => $spec) {
3757 $filterClass = $spec['filter'];
3758
3759 if($spec['filter'] instanceof SearchFilter) {
3760 $filters[$name] = $spec['filter'];
3761 } else {
3762 $class = $spec['filter'];
3763
3764 if(!is_subclass_of($spec['filter'], 'SearchFilter')) {
3765 $class = 'PartialMatchFilter';
3766 }
3767
3768 $filters[$name] = new $class($name);
3769 }
3770 }
3771
3772 return $filters;
3773 }
3774
3775 /**
3776 * @return boolean True if the object is in the database
3777 */
3778 public function isInDB() {
3779 return is_numeric( $this->ID ) && $this->ID > 0;
3780 }
3781
3782 /*
3783 * @ignore
3784 */
3785 private static $subclass_access = true;
3786
3787 /**
3788 * Temporarily disable subclass access in data object qeur
3789 */
3790 public static function disable_subclass_access() {
3791 self::$subclass_access = false;
3792 }
3793 public static function enable_subclass_access() {
3794 self::$subclass_access = true;
3795 }
3796
3797 //-------------------------------------------------------------------------------------------//
3798
3799 /**
3800 * Database field definitions.
3801 * This is a map from field names to field type. The field
3802 * type should be a class that extends .
3803 * @var array
3804 * @config
3805 */
3806 private static $db = null;
3807
3808 /**
3809 * Use a casting object for a field. This is a map from
3810 * field name to class name of the casting object.
3811 * @var array
3812 */
3813 private static $casting = array(
3814 "ID" => 'Int',
3815 "ClassName" => 'Varchar',
3816 "LastEdited" => "SS_Datetime",
3817 "Created" => "SS_Datetime",
3818 "Title" => 'Text',
3819 );
3820
3821 /**
3822 * Specify custom options for a CREATE TABLE call.
3823 * Can be used to specify a custom storage engine for specific database table.
3824 * All options have to be keyed for a specific database implementation,
3825 * identified by their class name (extending from {@link SS_Database}).
3826 *
3827 * <code>
3828 * array(
3829 * 'MySQLDatabase' => 'ENGINE=MyISAM'
3830 * )
3831 * </code>
3832 *
3833 * Caution: This API is experimental, and might not be
3834 * included in the next major release. Please use with care.
3835 *
3836 * @var array
3837 * @config
3838 */
3839 private static $create_table_options = array(
3840 'MySQLDatabase' => 'ENGINE=InnoDB'
3841 );
3842
3843 /**
3844 * If a field is in this array, then create a database index
3845 * on that field. This is a map from fieldname to index type.
3846 * See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation.
3847 *
3848 * @var array
3849 * @config
3850 */
3851 private static $indexes = null;
3852
3853 /**
3854 * Inserts standard column-values when a DataObject
3855 * is instanciated. Does not insert default records {@see $default_records}.
3856 * This is a map from fieldname to default value.
3857 *
3858 * - If you would like to change a default value in a sub-class, just specify it.
3859 * - If you would like to disable the default value given by a parent class, set the default value to 0,'',
3860 * or false in your subclass. Setting it to null won't work.
3861 *
3862 * @var array
3863 * @config
3864 */
3865 private static $defaults = null;
3866
3867 /**
3868 * Multidimensional array which inserts default data into the database
3869 * on a db/build-call as long as the database-table is empty. Please use this only
3870 * for simple constructs, not for SiteTree-Objects etc. which need special
3871 * behaviour such as publishing and ParentNodes.
3872 *
3873 * Example:
3874 * array(
3875 * array('Title' => "DefaultPage1", 'PageTitle' => 'page1'),
3876 * array('Title' => "DefaultPage2")
3877 * ).
3878 *
3879 * @var array
3880 * @config
3881 */
3882 private static $default_records = null;
3883
3884 /**
3885 * One-to-zero relationship defintion. This is a map of component name to data type. In order to turn this into a
3886 * true one-to-one relationship you can add a {@link DataObject::$belongs_to} relationship on the child class.
3887 *
3888 * Note that you cannot have a has_one and belongs_to relationship with the same name.
3889 *
3890 * @var array
3891 * @config
3892 */
3893 private static $has_one = null;
3894
3895 /**
3896 * A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}.
3897 *
3898 * This does not actually create any data structures, but allows you to query the other object in a one-to-one
3899 * relationship from the child object. If you have multiple belongs_to links to another object you can use the
3900 * syntax "ClassName.HasOneName" to specify which foreign has_one key on the other object to use.
3901 *
3902 * Note that you cannot have a has_one and belongs_to relationship with the same name.
3903 *
3904 * @var array
3905 * @config
3906 */
3907 private static $belongs_to;
3908
3909 /**
3910 * This defines a one-to-many relationship. It is a map of component name to the remote data class.
3911 *
3912 * This relationship type does not actually create a data structure itself - you need to define a matching $has_one
3913 * relationship on the child class. Also, if the $has_one relationship on the child class has multiple links to this
3914 * class you can use the syntax "ClassName.HasOneRelationshipName" in the remote data class definition to show
3915 * which foreign key to use.
3916 *
3917 * @var array
3918 * @config
3919 */
3920 private static $has_many = null;
3921
3922 /**
3923 * many-many relationship definitions.
3924 * This is a map from component name to data type.
3925 * @var array
3926 * @config
3927 */
3928 private static $many_many = null;
3929
3930 /**
3931 * Extra fields to include on the connecting many-many table.
3932 * This is a map from field name to field type.
3933 *
3934 * Example code:
3935 * <code>
3936 * public static $many_many_extraFields = array(
3937 * 'Members' => array(
3938 * 'Role' => 'Varchar(100)'
3939 * )
3940 * );
3941 * </code>
3942 *
3943 * @var array
3944 * @config
3945 */
3946 private static $many_many_extraFields = null;
3947
3948 /**
3949 * The inverse side of a many-many relationship.
3950 * This is a map from component name to data type.
3951 * @var array
3952 * @config
3953 */
3954 private static $belongs_many_many = null;
3955
3956 /**
3957 * The default sort expression. This will be inserted in the ORDER BY
3958 * clause of a SQL query if no other sort expression is provided.
3959 * @var string
3960 * @config
3961 */
3962 private static $default_sort = null;
3963
3964 /**
3965 * Default list of fields that can be scaffolded by the ModelAdmin
3966 * search interface.
3967 *
3968 * Overriding the default filter, with a custom defined filter:
3969 * <code>
3970 * static $searchable_fields = array(
3971 * "Name" => "PartialMatchFilter"
3972 * );
3973 * </code>
3974 *
3975 * Overriding the default form fields, with a custom defined field.
3976 * The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
3977 * The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
3978 * <code>
3979 * static $searchable_fields = array(
3980 * "Name" => array(
3981 * "field" => "TextField"
3982 * )
3983 * );
3984 * </code>
3985 *
3986 * Overriding the default form field, filter and title:
3987 * <code>
3988 * static $searchable_fields = array(
3989 * "Organisation.ZipCode" => array(
3990 * "field" => "TextField",
3991 * "filter" => "PartialMatchFilter",
3992 * "title" => 'Organisation ZIP'
3993 * )
3994 * );
3995 * </code>
3996 * @config
3997 */
3998 private static $searchable_fields = null;
3999
4000 /**
4001 * User defined labels for searchable_fields, used to override
4002 * default display in the search form.
4003 * @config
4004 */
4005 private static $field_labels = null;
4006
4007 /**
4008 * Provides a default list of fields to be used by a 'summary'
4009 * view of this object.
4010 * @config
4011 */
4012 private static $summary_fields = null;
4013
4014 /**
4015 * Provides a list of allowed methods that can be called via RESTful api.
4016 */
4017 public static $allowed_actions = null;
4018
4019 /**
4020 * Collect all static properties on the object
4021 * which contain natural language, and need to be translated.
4022 * The full entity name is composed from the class name and a custom identifier.
4023 *
4024 * @return array A numerical array which contains one or more entities in array-form.
4025 * Each numeric entity array contains the "arguments" for a _t() call as array values:
4026 * $entity, $string, $priority, $context.
4027 */
4028 public function provideI18nEntities() {
4029 $entities = array();
4030
4031 $entities["{$this->class}.SINGULARNAME"] = array(
4032 $this->singular_name(),
4033
4034 'Singular name of the object, used in dropdowns and to generally identify a single object in the interface'
4035 );
4036
4037 $entities["{$this->class}.PLURALNAME"] = array(
4038 $this->plural_name(),
4039
4040 'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the'
4041 . ' interface'
4042 );
4043
4044 return $entities;
4045 }
4046
4047 /**
4048 * Returns true if the given method/parameter has a value
4049 * (Uses the DBField::hasValue if the parameter is a database field)
4050 *
4051 * @param string $field The field name
4052 * @param array $arguments
4053 * @param bool $cache
4054 * @return boolean
4055 */
4056 public function hasValue($field, $arguments = null, $cache = true) {
4057 // has_one fields should not use dbObject to check if a value is given
4058 if(!$this->hasOneComponent($field) && ($obj = $this->dbObject($field))) {
4059 return $obj->exists();
4060 } else {
4061 return parent::hasValue($field, $arguments, $cache);
4062 }
4063 }
4064
4065}