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