2007-07-19 12:40:28 +02:00
|
|
|
<?php
|
2015-08-30 07:02:55 +02:00
|
|
|
|
2016-06-15 06:03:16 +02:00
|
|
|
namespace SilverStripe\ORM;
|
|
|
|
|
2017-12-01 16:03:39 +01:00
|
|
|
use BadMethodCallException;
|
|
|
|
use Exception;
|
|
|
|
use InvalidArgumentException;
|
|
|
|
use LogicException;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\Core\ClassInfo;
|
|
|
|
use SilverStripe\Core\Config\Config;
|
|
|
|
use SilverStripe\Core\Injector\Injector;
|
2017-01-26 05:20:08 +01:00
|
|
|
use SilverStripe\Core\Resettable;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\Dev\Debug;
|
2017-12-01 16:03:39 +01:00
|
|
|
use SilverStripe\Dev\Deprecation;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\Forms\FieldList;
|
|
|
|
use SilverStripe\Forms\FormField;
|
|
|
|
use SilverStripe\Forms\FormScaffolder;
|
2020-05-27 23:35:07 +02:00
|
|
|
use SilverStripe\Forms\CompositeValidator;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\i18n\i18n;
|
|
|
|
use SilverStripe\i18n\i18nEntityProvider;
|
2017-07-19 13:28:15 +02:00
|
|
|
use SilverStripe\ORM\Connect\MySQLSchemaManager;
|
2017-12-01 16:03:39 +01:00
|
|
|
use SilverStripe\ORM\FieldType\DBComposite;
|
|
|
|
use SilverStripe\ORM\FieldType\DBDatetime;
|
2020-05-03 23:10:51 +02:00
|
|
|
use SilverStripe\ORM\FieldType\DBEnum;
|
2017-12-01 16:03:39 +01:00
|
|
|
use SilverStripe\ORM\FieldType\DBField;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\ORM\Filters\SearchFilter;
|
2016-06-15 06:03:16 +02:00
|
|
|
use SilverStripe\ORM\Queries\SQLDelete;
|
2017-12-01 16:03:39 +01:00
|
|
|
use SilverStripe\ORM\Search\SearchContext;
|
2020-10-28 21:29:26 +01:00
|
|
|
use SilverStripe\ORM\RelatedData\RelatedDataService;
|
2020-05-03 23:10:51 +02:00
|
|
|
use SilverStripe\ORM\UniqueKey\UniqueKeyInterface;
|
|
|
|
use SilverStripe\ORM\UniqueKey\UniqueKeyService;
|
2016-06-23 01:37:22 +02:00
|
|
|
use SilverStripe\Security\Member;
|
|
|
|
use SilverStripe\Security\Permission;
|
2017-05-20 06:32:25 +02:00
|
|
|
use SilverStripe\Security\Security;
|
2017-12-01 16:03:39 +01:00
|
|
|
use SilverStripe\View\SSViewer;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\View\ViewableData;
|
2017-05-17 07:40:13 +02:00
|
|
|
use stdClass;
|
2015-08-30 07:02:55 +02:00
|
|
|
|
2007-07-19 12:40:28 +02:00
|
|
|
/**
|
|
|
|
* A single database record & abstract class for the data-access-model.
|
2008-09-22 18:02:03 +02:00
|
|
|
*
|
2011-04-15 11:35:30 +02:00
|
|
|
* <h2>Extensions</h2>
|
2009-03-31 21:34:37 +02:00
|
|
|
*
|
2011-04-15 11:35:30 +02:00
|
|
|
* See {@link Extension} and {@link DataExtension}.
|
2013-06-21 00:32:08 +02:00
|
|
|
*
|
2009-03-31 21:34:37 +02:00
|
|
|
* <h2>Permission Control</h2>
|
2013-06-21 00:32:08 +02:00
|
|
|
*
|
2008-09-22 18:02:03 +02:00
|
|
|
* Object-level access control by {@link Permission}. Permission codes are arbitrary
|
|
|
|
* strings which can be selected on a group-by-group basis.
|
2013-06-21 00:32:08 +02:00
|
|
|
*
|
2008-09-22 18:02:03 +02:00
|
|
|
* <code>
|
|
|
|
* class Article extends DataObject implements PermissionProvider {
|
2012-12-08 12:20:20 +01:00
|
|
|
* static $api_access = true;
|
2013-06-21 00:32:08 +02:00
|
|
|
*
|
2012-12-08 12:20:20 +01:00
|
|
|
* function canView($member = false) {
|
|
|
|
* return Permission::check('ARTICLE_VIEW');
|
|
|
|
* }
|
|
|
|
* function canEdit($member = false) {
|
|
|
|
* return Permission::check('ARTICLE_EDIT');
|
|
|
|
* }
|
|
|
|
* function canDelete() {
|
|
|
|
* return Permission::check('ARTICLE_DELETE');
|
|
|
|
* }
|
|
|
|
* function canCreate() {
|
|
|
|
* return Permission::check('ARTICLE_CREATE');
|
|
|
|
* }
|
|
|
|
* function providePermissions() {
|
|
|
|
* return array(
|
|
|
|
* 'ARTICLE_VIEW' => 'Read an article object',
|
|
|
|
* 'ARTICLE_EDIT' => 'Edit an article object',
|
|
|
|
* 'ARTICLE_DELETE' => 'Delete an article object',
|
|
|
|
* 'ARTICLE_CREATE' => 'Create an article object',
|
|
|
|
* );
|
|
|
|
* }
|
2008-09-22 18:02:03 +02:00
|
|
|
* }
|
2013-06-21 00:32:08 +02:00
|
|
|
* </code>
|
2008-09-22 18:02:03 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* Object-level access control by {@link Group} membership:
|
2008-09-22 18:02:03 +02:00
|
|
|
* <code>
|
|
|
|
* class Article extends DataObject {
|
2012-12-08 12:20:20 +01:00
|
|
|
* static $api_access = true;
|
2013-06-21 00:32:08 +02:00
|
|
|
*
|
2012-12-08 12:20:20 +01:00
|
|
|
* function canView($member = false) {
|
2017-05-20 06:32:25 +02:00
|
|
|
* if(!$member) $member = Security::getCurrentUser();
|
2012-12-08 12:20:20 +01:00
|
|
|
* return $member->inGroup('Subscribers');
|
|
|
|
* }
|
|
|
|
* function canEdit($member = false) {
|
2017-05-20 06:32:25 +02:00
|
|
|
* if(!$member) $member = Security::getCurrentUser();
|
2012-12-08 12:20:20 +01:00
|
|
|
* return $member->inGroup('Editors');
|
|
|
|
* }
|
2013-06-21 00:32:08 +02:00
|
|
|
*
|
2012-12-08 12:20:20 +01:00
|
|
|
* // ...
|
2008-09-22 18:02:03 +02:00
|
|
|
* }
|
|
|
|
* </code>
|
2013-06-21 00:32:08 +02:00
|
|
|
*
|
|
|
|
* If any public method on this class is prefixed with an underscore,
|
2009-03-17 21:20:43 +01:00
|
|
|
* the results are cached in memory through {@link cachedCall()}.
|
2013-06-21 00:32:08 +02:00
|
|
|
*
|
|
|
|
*
|
2009-03-31 21:34:37 +02:00
|
|
|
* @todo Add instance specific removeExtension() which undos loadExtraStatics()
|
|
|
|
* and defineMethods()
|
2013-06-21 00:32:08 +02:00
|
|
|
*
|
2017-06-15 04:20:12 +02:00
|
|
|
* @property int $ID ID of the DataObject, 0 if the DataObject doesn't exist in database.
|
|
|
|
* @property int $OldID ID of object, if deleted
|
2017-12-12 00:34:53 +01:00
|
|
|
* @property string $Title
|
2017-06-15 04:20:12 +02:00
|
|
|
* @property string $ClassName Class name of the DataObject
|
|
|
|
* @property string $LastEdited Date and time of DataObject's last modification.
|
|
|
|
* @property string $Created Date and time of DataObject creation.
|
2018-03-21 22:26:25 +01:00
|
|
|
* @property string $ObsoleteClassName If ClassName no longer exists this will be set to the legacy value
|
2007-07-19 12:40:28 +02:00
|
|
|
*/
|
2017-01-26 05:20:08 +01:00
|
|
|
class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider, Resettable
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Human-readable singular name.
|
|
|
|
* @var string
|
|
|
|
* @config
|
|
|
|
*/
|
|
|
|
private static $singular_name = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Human-readable plural name
|
|
|
|
* @var string
|
|
|
|
* @config
|
|
|
|
*/
|
|
|
|
private static $plural_name = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Allow API access to this object?
|
|
|
|
* @todo Define the options that can be set here
|
|
|
|
* @config
|
|
|
|
*/
|
|
|
|
private static $api_access = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Allows specification of a default value for the ClassName field.
|
|
|
|
* Configure this value only in subclasses of DataObject.
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private static $default_classname = null;
|
|
|
|
|
|
|
|
/**
|
2018-09-28 10:46:36 +02:00
|
|
|
* @deprecated 4.0.0:5.0.0
|
2017-09-03 23:23:07 +02:00
|
|
|
* @var bool
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public $destroyed = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Data stored in this objects database record. An array indexed by fieldname.
|
|
|
|
*
|
|
|
|
* Use {@link toMap()} if you want an array representation
|
|
|
|
* of this object, as the $record array might contain lazy loaded field aliases.
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
protected $record;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If selected through a many_many through relation, this is the instance of the through record
|
|
|
|
*
|
|
|
|
* @var DataObject
|
|
|
|
*/
|
|
|
|
protected $joinRecord;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Represents a field that hasn't changed (before === after, thus before == after)
|
|
|
|
*/
|
|
|
|
const CHANGE_NONE = 0;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Represents a field that has changed type, although not the loosely defined value.
|
|
|
|
* (before !== after && before == after)
|
|
|
|
* E.g. change 1 to true or "true" to true, but not true to 0.
|
|
|
|
* Value changes are by nature also considered strict changes.
|
|
|
|
*/
|
|
|
|
const CHANGE_STRICT = 1;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Represents a field that has changed the loosely defined value
|
|
|
|
* (before != after, thus, before !== after))
|
|
|
|
* E.g. change false to true, but not false to 0
|
|
|
|
*/
|
|
|
|
const CHANGE_VALUE = 2;
|
|
|
|
|
2020-08-20 02:28:31 +02:00
|
|
|
/**
|
|
|
|
* Value for 2nd argument to constructor, indicating that a new record is being created
|
|
|
|
* Setters will be called on fields passed, and defaults will be populated
|
|
|
|
*/
|
|
|
|
const CREATE_OBJECT = 0;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Value for 2nd argument to constructor, indicating that a record is a singleton representing the whole type,
|
|
|
|
* e.g. to call requireTable() in dev/build
|
|
|
|
* Defaults will not be populated and data passed will be ignored
|
|
|
|
*/
|
|
|
|
const CREATE_SINGLETON = 1;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Value for 2nd argument to constructor, indicating that a record is being hydrated from the database
|
2021-01-21 02:07:06 +01:00
|
|
|
* Setter methods are not called, and population via private static $defaults will not occur.
|
2020-08-20 02:28:31 +02:00
|
|
|
*/
|
|
|
|
const CREATE_HYDRATED = 2;
|
|
|
|
|
2021-01-21 02:07:06 +01:00
|
|
|
/**
|
|
|
|
* Value for 2nd argument to constructor, indicating that a record is being hydrated from memory. This can be used
|
|
|
|
* to initialised a record that doesn't yet have an ID. Setter methods are not called, and population via private
|
|
|
|
* static $defaults will not occur.
|
|
|
|
*/
|
|
|
|
const CREATE_MEMORY_HYDRATED = 3;
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* An array indexed by fieldname, true if the field has been changed.
|
|
|
|
* Use {@link getChangedFields()} and {@link isChanged()} to inspect
|
|
|
|
* the changed state.
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
*/
|
2018-10-08 03:55:02 +02:00
|
|
|
private $changed = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A flag to indicate that a "strict" change of the entire record been forced
|
|
|
|
* Use {@link getChangedFields()} and {@link isChanged()} to inspect
|
|
|
|
* the changed state.
|
|
|
|
*
|
|
|
|
* @var boolean
|
|
|
|
*/
|
|
|
|
private $changeForced = false;
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The database record (in the same format as $record), before
|
|
|
|
* any changes.
|
|
|
|
* @var array
|
|
|
|
*/
|
2018-10-08 03:55:02 +02:00
|
|
|
protected $original = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Used by onBeforeDelete() to ensure child classes call parent::onBeforeDelete()
|
|
|
|
* @var boolean
|
|
|
|
*/
|
|
|
|
protected $brokenOnDelete = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite()
|
|
|
|
* @var boolean
|
|
|
|
*/
|
|
|
|
protected $brokenOnWrite = false;
|
|
|
|
|
|
|
|
/**
|
2018-08-29 04:52:49 +02:00
|
|
|
* Should dataobjects be validated before they are written?
|
|
|
|
*
|
2016-11-29 00:31:16 +01:00
|
|
|
* Caution: Validation can contain safeguards against invalid/malicious data,
|
|
|
|
* and check permission levels (e.g. on {@link Group}). Therefore it is recommended
|
|
|
|
* to only disable validation for very specific use cases.
|
2018-08-29 04:52:49 +02:00
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var boolean
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
private static $validation_enabled = true;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Static caches used by relevant functions.
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
protected static $_cache_get_one;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Cache of field labels
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
*/
|
2020-04-20 19:58:09 +02:00
|
|
|
protected static $_cache_field_labels = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Base fields which are not defined in static $db
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var array
|
|
|
|
*/
|
2020-04-20 19:58:09 +02:00
|
|
|
private static $fixed_fields = [
|
2016-11-29 00:31:16 +01:00
|
|
|
'ID' => 'PrimaryKey',
|
|
|
|
'ClassName' => 'DBClassName',
|
|
|
|
'LastEdited' => 'DBDatetime',
|
|
|
|
'Created' => 'DBDatetime',
|
2020-04-20 19:58:09 +02:00
|
|
|
];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Override table name for this class. If ignored will default to FQN of class.
|
|
|
|
* This option is not inheritable, and must be set on each class.
|
|
|
|
* If left blank naming will default to the legacy (3.x) behaviour.
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private static $table_name = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Non-static relationship cache, indexed by component name.
|
|
|
|
*
|
|
|
|
* @var DataObject[]
|
|
|
|
*/
|
2018-01-30 06:28:28 +01:00
|
|
|
protected $components = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Non-static cache of has_many and many_many relations that can't be written until this object is saved.
|
|
|
|
*
|
|
|
|
* @var UnsavedRelationList[]
|
|
|
|
*/
|
|
|
|
protected $unsavedRelations;
|
|
|
|
|
2017-08-08 06:46:34 +02:00
|
|
|
/**
|
|
|
|
* List of relations that should be cascade deleted, similar to `owns`
|
|
|
|
* Note: This will trigger delete on many_many objects, not only the mapping table.
|
|
|
|
* For many_many through you can specify the components you want to delete separately
|
|
|
|
* (many_many or has_many sub-component)
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
private static $cascade_deletes = [];
|
|
|
|
|
2018-01-30 06:28:28 +01:00
|
|
|
/**
|
|
|
|
* List of relations that should be cascade duplicate.
|
|
|
|
* many_many duplications are shallow only.
|
|
|
|
*
|
|
|
|
* Note: If duplicating a many_many through you should refer to the
|
|
|
|
* has_many intermediary relation instead, otherwise extra fields
|
|
|
|
* will be omitted from the duplicated relation.
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
private static $cascade_duplicates = [];
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* Get schema object
|
|
|
|
*
|
|
|
|
* @return DataObjectSchema
|
|
|
|
*/
|
|
|
|
public static function getSchema()
|
|
|
|
{
|
|
|
|
return Injector::inst()->get(DataObjectSchema::class);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Construct a new DataObject.
|
|
|
|
*
|
2020-08-20 02:28:31 +02:00
|
|
|
* @param array $record Initial record content, or rehydrated record content, depending on $creationType
|
2021-01-21 02:07:06 +01:00
|
|
|
* @param int|boolean $creationType Set to DataObject::CREATE_OBJECT, DataObject::CREATE_HYDRATED,
|
|
|
|
* DataObject::CREATE_MEMORY_HYDRATED or DataObject::CREATE_SINGLETON. Used by Silverstripe internals and best
|
|
|
|
* left as the default by regular users.
|
2016-11-29 00:31:16 +01:00
|
|
|
* @param array $queryParams List of DataQuery params necessary to lazy load, or load related objects.
|
|
|
|
*/
|
2020-08-20 02:28:31 +02:00
|
|
|
public function __construct($record = [], $creationType = self::CREATE_OBJECT, $queryParams = [])
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
|
|
|
parent::__construct();
|
|
|
|
|
2020-08-20 02:28:31 +02:00
|
|
|
// Legacy $record default
|
|
|
|
if ($record === null) {
|
|
|
|
$record = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
2020-08-20 02:28:31 +02:00
|
|
|
// Legacy $isSingleton boolean
|
|
|
|
if (!is_int($creationType)) {
|
|
|
|
if (!is_bool($creationType)) {
|
|
|
|
user_error('Creation type is neither boolean (old isSingleton arg) nor integer (new arg), please review your code', E_USER_WARNING);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
2020-08-20 02:28:31 +02:00
|
|
|
$creationType = $creationType ? self::CREATE_SINGLETON : self::CREATE_OBJECT;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
2020-08-20 02:28:31 +02:00
|
|
|
// Set query params on the DataObject to tell the lazy loading mechanism the context the object creation context
|
|
|
|
$this->setSourceQueryParams($queryParams);
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
// Set $this->record to $record, but ignore NULLs
|
2020-04-20 19:58:09 +02:00
|
|
|
$this->record = [];
|
2020-08-20 02:28:31 +02:00
|
|
|
|
|
|
|
switch ($creationType) {
|
2021-01-21 02:07:06 +01:00
|
|
|
// Hydrate a record
|
2020-08-20 02:28:31 +02:00
|
|
|
case self::CREATE_HYDRATED:
|
2021-01-21 02:07:06 +01:00
|
|
|
case self::CREATE_MEMORY_HYDRATED:
|
|
|
|
$this->hydrate($record, $creationType === self::CREATE_HYDRATED);
|
2020-08-20 02:28:31 +02:00
|
|
|
break;
|
|
|
|
|
|
|
|
// Create a new object, using the constructor argument as the initial content
|
|
|
|
case self::CREATE_OBJECT:
|
|
|
|
if ($record instanceof stdClass) {
|
|
|
|
$record = (array)$record;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_array($record)) {
|
|
|
|
if (is_object($record)) {
|
|
|
|
$passed = "an object of type '" . get_class($record) . "'";
|
|
|
|
} else {
|
|
|
|
$passed = "The value '$record'";
|
|
|
|
}
|
|
|
|
|
|
|
|
user_error(
|
|
|
|
"DataObject::__construct passed $passed. It's supposed to be passed an array,"
|
|
|
|
. " taken straight from the database. Perhaps you should use DataList::create()->First(); instead?",
|
|
|
|
E_USER_WARNING
|
|
|
|
);
|
|
|
|
$record = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Default columns
|
|
|
|
$this->record['ID'] = empty($record['ID']) ? 0 : $record['ID'];
|
|
|
|
$this->record['ClassName'] = static::class;
|
|
|
|
$this->record['RecordClassName'] = static::class;
|
|
|
|
unset($record['ID']);
|
|
|
|
$this->original = $this->record;
|
|
|
|
|
|
|
|
$this->populateDefaults();
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2020-08-20 02:28:31 +02:00
|
|
|
// prevent populateDefaults() and setField() from marking overwritten defaults as changed
|
|
|
|
$this->changed = [];
|
|
|
|
$this->changeForced = false;
|
|
|
|
|
|
|
|
// Set the data passed in the constructor, allowing for defaults and calling setters
|
|
|
|
// This will mark fields as changed
|
|
|
|
if ($record) {
|
|
|
|
$this->update($record);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case self::CREATE_SINGLETON:
|
|
|
|
// No setting happens for a singleton
|
|
|
|
$this->record['ID'] = 0;
|
|
|
|
$this->record['ClassName'] = static::class;
|
|
|
|
$this->record['RecordClassName'] = static::class;
|
|
|
|
$this->original = $this->record;
|
|
|
|
$this->changed = [];
|
|
|
|
$this->changeForced = false;
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
throw new \LogicException('Bad creationType ' . $this->creationType);
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
2021-01-21 02:07:06 +01:00
|
|
|
/**
|
|
|
|
* Constructor hydration logic for CREATE_HYDRATED and CREATE_MEMORY_HYDRATED.
|
|
|
|
* @param array $record
|
|
|
|
* @param bool $mustHaveID If true, an exception will be thrown if $record doesn't have an ID.
|
|
|
|
*/
|
|
|
|
private function hydrate(array $record, bool $mustHaveID)
|
|
|
|
{
|
|
|
|
if ($mustHaveID && empty($record['ID'])) {
|
|
|
|
// CREATE_HYDRATED requires an ID to be included in the record
|
|
|
|
throw new \InvalidArgumentException(
|
|
|
|
"Hydrated records must be passed a record array including an ID."
|
|
|
|
);
|
|
|
|
} elseif (empty($record['ID'])) {
|
|
|
|
// CREATE_MEMORY_HYDRATED implicitely set the record ID to 0 if not provided
|
|
|
|
$record['ID'] = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->record = $record;
|
|
|
|
|
|
|
|
// Identify fields that should be lazy loaded, but only on existing records
|
|
|
|
// Get all field specs scoped to class for later lazy loading
|
|
|
|
$fields = static::getSchema()->fieldSpecs(
|
|
|
|
static::class,
|
|
|
|
DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY
|
|
|
|
);
|
|
|
|
foreach ($fields as $field => $fieldSpec) {
|
|
|
|
$fieldClass = strtok($fieldSpec, ".");
|
|
|
|
if (!array_key_exists($field, $record)) {
|
|
|
|
$this->record[$field . '_Lazy'] = $fieldClass;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->original = $this->record;
|
|
|
|
$this->changed = [];
|
|
|
|
$this->changeForced = false;
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* Destroy all of this objects dependant objects and local caches.
|
|
|
|
* You'll need to call this to get the memory of an object that has components or extensions freed.
|
|
|
|
*/
|
|
|
|
public function destroy()
|
|
|
|
{
|
|
|
|
$this->flushCache(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-10-11 06:50:19 +02:00
|
|
|
* Create a duplicate of this node. Can duplicate many_many relations
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
|
|
|
* @param bool $doWrite Perform a write() operation before returning the object.
|
|
|
|
* If this is true, it will create the duplicate in the database.
|
2018-01-30 06:28:28 +01:00
|
|
|
* @param array|null|false $relations List of relations to duplicate.
|
|
|
|
* Will default to `cascade_duplicates` if null.
|
|
|
|
* Set to 'false' to force none.
|
|
|
|
* Set to specific array of names to duplicate to override these.
|
|
|
|
* Note: If using versioned, this will additionally failover to `owns` config.
|
2016-10-11 06:50:19 +02:00
|
|
|
* @return static A duplicate of this node. The exact type will be the type of this node.
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2018-01-30 06:28:28 +01:00
|
|
|
public function duplicate($doWrite = true, $relations = null)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2018-01-30 06:28:28 +01:00
|
|
|
// Handle legacy behaviour
|
|
|
|
if (is_string($relations) || $relations === true) {
|
|
|
|
if ($relations === true) {
|
|
|
|
$relations = 'many_many';
|
|
|
|
}
|
|
|
|
Deprecation::notice('5.0', 'Use cascade_duplicates config instead of providing a string to duplicate()');
|
|
|
|
$relations = array_keys($this->config()->get($relations)) ?: [];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get duplicates
|
|
|
|
if ($relations === null) {
|
|
|
|
$relations = $this->config()->get('cascade_duplicates');
|
2018-07-13 00:33:12 +02:00
|
|
|
// Remove any duplicate entries before duplicating them
|
|
|
|
if (is_array($relations)) {
|
|
|
|
$relations = array_unique($relations);
|
|
|
|
}
|
2018-01-30 06:28:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create unsaved raw duplicate
|
2016-12-28 14:52:49 +01:00
|
|
|
$map = $this->toMap();
|
|
|
|
unset($map['Created']);
|
2016-11-29 00:31:16 +01:00
|
|
|
/** @var static $clone */
|
2017-06-22 12:50:45 +02:00
|
|
|
$clone = Injector::inst()->create(static::class, $map, false, $this->getSourceQueryParams());
|
2016-11-29 00:31:16 +01:00
|
|
|
$clone->ID = 0;
|
|
|
|
|
2018-01-30 06:28:28 +01:00
|
|
|
// Note: Extensions such as versioned may update $relations here
|
|
|
|
$clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite, $relations);
|
|
|
|
if ($relations) {
|
|
|
|
$this->duplicateRelations($this, $clone, $relations);
|
2016-10-11 06:50:19 +02:00
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
if ($doWrite) {
|
|
|
|
$clone->write();
|
|
|
|
}
|
2018-01-30 06:28:28 +01:00
|
|
|
$clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite, $relations);
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
return $clone;
|
|
|
|
}
|
|
|
|
|
2018-01-30 06:28:28 +01:00
|
|
|
/**
|
|
|
|
* Copies the given relations from this object to the destination
|
|
|
|
*
|
|
|
|
* @param DataObject $sourceObject the source object to duplicate from
|
|
|
|
* @param DataObject $destinationObject the destination object to populate with the duplicated relations
|
|
|
|
* @param array $relations List of relations
|
|
|
|
*/
|
|
|
|
protected function duplicateRelations($sourceObject, $destinationObject, $relations)
|
|
|
|
{
|
|
|
|
// Get list of duplicable relation types
|
|
|
|
$manyMany = $sourceObject->manyMany();
|
|
|
|
$hasMany = $sourceObject->hasMany();
|
|
|
|
$hasOne = $sourceObject->hasOne();
|
|
|
|
$belongsTo = $sourceObject->belongsTo();
|
|
|
|
|
|
|
|
// Duplicate each relation based on type
|
|
|
|
foreach ($relations as $relation) {
|
|
|
|
switch (true) {
|
|
|
|
case array_key_exists($relation, $manyMany): {
|
|
|
|
$this->duplicateManyManyRelation($sourceObject, $destinationObject, $relation);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case array_key_exists($relation, $hasMany): {
|
|
|
|
$this->duplicateHasManyRelation($sourceObject, $destinationObject, $relation);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case array_key_exists($relation, $hasOne): {
|
|
|
|
$this->duplicateHasOneRelation($sourceObject, $destinationObject, $relation);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case array_key_exists($relation, $belongsTo): {
|
|
|
|
$this->duplicateBelongsToRelation($sourceObject, $destinationObject, $relation);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default: {
|
|
|
|
$sourceType = get_class($sourceObject);
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
"Cannot duplicate unknown relation {$relation} on parent type {$sourceType}"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
2016-10-11 06:50:19 +02:00
|
|
|
* Copies the many_many and belongs_many_many relations from one object to another instance of the name of object.
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
2018-09-28 10:46:36 +02:00
|
|
|
* @deprecated 4.1.0:5.0.0 Use duplicateRelations() instead
|
2016-11-29 00:31:16 +01:00
|
|
|
* @param DataObject $sourceObject the source object to duplicate from
|
|
|
|
* @param DataObject $destinationObject the destination object to populate with the duplicated relations
|
2016-10-11 06:50:19 +02:00
|
|
|
* @param bool|string $filter
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2016-10-11 06:50:19 +02:00
|
|
|
protected function duplicateManyManyRelations($sourceObject, $destinationObject, $filter)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2018-01-30 06:28:28 +01:00
|
|
|
Deprecation::notice('5.0', 'Use duplicateRelations() instead');
|
|
|
|
|
2016-10-11 06:50:19 +02:00
|
|
|
// Get list of relations to duplicate
|
|
|
|
if ($filter === 'many_many' || $filter === 'belongs_many_many') {
|
|
|
|
$relations = $sourceObject->config()->get($filter);
|
|
|
|
} elseif ($filter === true) {
|
|
|
|
$relations = $sourceObject->manyMany();
|
|
|
|
} else {
|
|
|
|
throw new InvalidArgumentException("Invalid many_many duplication filter");
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
2016-10-11 06:50:19 +02:00
|
|
|
foreach ($relations as $manyManyName => $type) {
|
|
|
|
$this->duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-01-30 06:28:28 +01:00
|
|
|
* Duplicates a single many_many relation from one object to another.
|
2016-10-11 06:50:19 +02:00
|
|
|
*
|
|
|
|
* @param DataObject $sourceObject
|
|
|
|
* @param DataObject $destinationObject
|
2018-01-30 06:28:28 +01:00
|
|
|
* @param string $relation
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2018-01-30 06:28:28 +01:00
|
|
|
protected function duplicateManyManyRelation($sourceObject, $destinationObject, $relation)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2018-01-30 06:28:28 +01:00
|
|
|
// Copy all components from source to destination
|
|
|
|
$source = $sourceObject->getManyManyComponents($relation);
|
|
|
|
$dest = $destinationObject->getManyManyComponents($relation);
|
2018-04-18 15:45:51 +02:00
|
|
|
|
2018-04-18 17:15:16 +02:00
|
|
|
if ($source instanceof ManyManyList) {
|
2018-04-18 15:45:51 +02:00
|
|
|
$extraFieldNames = $source->getExtraFields();
|
2018-04-18 17:15:16 +02:00
|
|
|
} else {
|
2018-05-30 04:52:07 +02:00
|
|
|
$extraFieldNames = [];
|
2018-04-18 15:45:51 +02:00
|
|
|
}
|
|
|
|
|
2018-01-30 06:28:28 +01:00
|
|
|
foreach ($source as $item) {
|
|
|
|
// Merge extra fields
|
|
|
|
$extraFields = [];
|
|
|
|
foreach ($extraFieldNames as $fieldName => $fieldType) {
|
|
|
|
$extraFields[$fieldName] = $item->getField($fieldName);
|
|
|
|
}
|
|
|
|
$dest->add($item, $extraFields);
|
2016-10-11 06:50:19 +02:00
|
|
|
}
|
2018-01-30 06:28:28 +01:00
|
|
|
}
|
2016-10-11 06:50:19 +02:00
|
|
|
|
2018-01-30 06:28:28 +01:00
|
|
|
/**
|
|
|
|
* Duplicates a single many_many relation from one object to another.
|
|
|
|
*
|
|
|
|
* @param DataObject $sourceObject
|
|
|
|
* @param DataObject $destinationObject
|
|
|
|
* @param string $relation
|
|
|
|
*/
|
|
|
|
protected function duplicateHasManyRelation($sourceObject, $destinationObject, $relation)
|
|
|
|
{
|
2016-10-11 06:50:19 +02:00
|
|
|
// Copy all components from source to destination
|
2018-01-30 06:28:28 +01:00
|
|
|
$source = $sourceObject->getComponents($relation);
|
|
|
|
$dest = $destinationObject->getComponents($relation);
|
|
|
|
|
|
|
|
/** @var DataObject $item */
|
2016-10-11 06:50:19 +02:00
|
|
|
foreach ($source as $item) {
|
2018-01-30 06:28:28 +01:00
|
|
|
// Don't write on duplicate; Wait until ParentID is available later.
|
|
|
|
// writeRelations() will eventually write these records when converting
|
|
|
|
// from UnsavedRelationList
|
|
|
|
$clonedItem = $item->duplicate(false);
|
|
|
|
$dest->add($clonedItem);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Duplicates a single has_one relation from one object to another.
|
|
|
|
* Note: Child object will be force written.
|
|
|
|
*
|
|
|
|
* @param DataObject $sourceObject
|
|
|
|
* @param DataObject $destinationObject
|
|
|
|
* @param string $relation
|
|
|
|
*/
|
|
|
|
protected function duplicateHasOneRelation($sourceObject, $destinationObject, $relation)
|
|
|
|
{
|
|
|
|
// Check if original object exists
|
|
|
|
$item = $sourceObject->getComponent($relation);
|
|
|
|
if (!$item->isInDB()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$clonedItem = $item->duplicate(false);
|
|
|
|
$destinationObject->setComponent($relation, $clonedItem);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Duplicates a single belongs_to relation from one object to another.
|
|
|
|
* Note: This will force a write on both parent / child objects.
|
|
|
|
*
|
|
|
|
* @param DataObject $sourceObject
|
|
|
|
* @param DataObject $destinationObject
|
|
|
|
* @param string $relation
|
|
|
|
*/
|
|
|
|
protected function duplicateBelongsToRelation($sourceObject, $destinationObject, $relation)
|
|
|
|
{
|
|
|
|
// Check if original object exists
|
|
|
|
$item = $sourceObject->getComponent($relation);
|
|
|
|
if (!$item->isInDB()) {
|
|
|
|
return;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
2018-01-30 06:28:28 +01:00
|
|
|
|
|
|
|
$clonedItem = $item->duplicate(false);
|
|
|
|
$destinationObject->setComponent($relation, $clonedItem);
|
|
|
|
// After $clonedItem is assigned the appropriate FieldID / FieldClass, force write
|
|
|
|
// @todo Write this component in onAfterWrite instead, assigning the FieldID then
|
|
|
|
// https://github.com/silverstripe/silverstripe-framework/issues/7818
|
|
|
|
$clonedItem->write();
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return obsolete class name, if this is no longer a valid class
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getObsoleteClassName()
|
|
|
|
{
|
|
|
|
$className = $this->getField("ClassName");
|
|
|
|
if (!ClassInfo::exists($className)) {
|
|
|
|
return $className;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets name of this class
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getClassName()
|
|
|
|
{
|
|
|
|
$className = $this->getField("ClassName");
|
|
|
|
if (!ClassInfo::exists($className)) {
|
|
|
|
return static::class;
|
|
|
|
}
|
|
|
|
return $className;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the ClassName attribute. {@link $class} is also updated.
|
|
|
|
* Warning: This will produce an inconsistent record, as the object
|
|
|
|
* instance will not automatically switch to the new subclass.
|
|
|
|
* Please use {@link newClassInstance()} for this purpose,
|
|
|
|
* or destroy and reinstanciate the record.
|
|
|
|
*
|
|
|
|
* @param string $className The new ClassName attribute (a subclass of {@link DataObject})
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setClassName($className)
|
|
|
|
{
|
|
|
|
$className = trim($className);
|
|
|
|
if (!$className || !is_subclass_of($className, self::class)) {
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->setField("ClassName", $className);
|
|
|
|
$this->setField('RecordClassName', $className);
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a new instance of a different class from this object's record.
|
|
|
|
* This is useful when dynamically changing the type of an instance. Specifically,
|
|
|
|
* it ensures that the instance of the class is a match for the className of the
|
|
|
|
* record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
|
|
|
|
* property manually before calling this method, as it will confuse change detection.
|
|
|
|
*
|
|
|
|
* If the new class is different to the original class, defaults are populated again
|
|
|
|
* because this will only occur automatically on instantiation of a DataObject if
|
|
|
|
* there is no record, or the record has no ID. In this case, we do have an ID but
|
|
|
|
* we still need to repopulate the defaults.
|
|
|
|
*
|
|
|
|
* @param string $newClassName The name of the new class
|
|
|
|
*
|
|
|
|
* @return DataObject The new instance of the new class, The exact type will be of the class name provided.
|
|
|
|
*/
|
|
|
|
public function newClassInstance($newClassName)
|
|
|
|
{
|
|
|
|
if (!is_subclass_of($newClassName, self::class)) {
|
|
|
|
throw new InvalidArgumentException("$newClassName is not a valid subclass of DataObject");
|
|
|
|
}
|
|
|
|
|
|
|
|
$originalClass = $this->ClassName;
|
|
|
|
|
|
|
|
/** @var DataObject $newInstance */
|
2021-01-21 02:07:06 +01:00
|
|
|
$newInstance = Injector::inst()->create($newClassName, $this->record, self::CREATE_MEMORY_HYDRATED);
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
// Modify ClassName
|
|
|
|
if ($newClassName != $originalClass) {
|
|
|
|
$newInstance->setClassName($newClassName);
|
|
|
|
$newInstance->populateDefaults();
|
|
|
|
$newInstance->forceChange();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $newInstance;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds methods from the extensions.
|
|
|
|
* Called by Object::__construct() once per class.
|
|
|
|
*/
|
|
|
|
public function defineMethods()
|
|
|
|
{
|
|
|
|
parent::defineMethods();
|
|
|
|
|
|
|
|
if (static::class === self::class) {
|
2018-01-30 06:28:28 +01:00
|
|
|
return;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Set up accessors for joined items
|
|
|
|
if ($manyMany = $this->manyMany()) {
|
|
|
|
foreach ($manyMany as $relationship => $class) {
|
|
|
|
$this->addWrapperMethod($relationship, 'getManyManyComponents');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($hasMany = $this->hasMany()) {
|
|
|
|
foreach ($hasMany as $relationship => $class) {
|
|
|
|
$this->addWrapperMethod($relationship, 'getComponents');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($hasOne = $this->hasOne()) {
|
|
|
|
foreach ($hasOne as $relationship => $class) {
|
|
|
|
$this->addWrapperMethod($relationship, 'getComponent');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($belongsTo = $this->belongsTo()) {
|
|
|
|
foreach (array_keys($belongsTo) as $relationship) {
|
|
|
|
$this->addWrapperMethod($relationship, 'getComponent');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if this object "exists", i.e., has a sensible value.
|
|
|
|
* The default behaviour for a DataObject is to return true if
|
|
|
|
* the object exists in the database, you can override this in subclasses.
|
|
|
|
*
|
|
|
|
* @return boolean true if this object exists
|
|
|
|
*/
|
|
|
|
public function exists()
|
|
|
|
{
|
|
|
|
return (isset($this->record['ID']) && $this->record['ID'] > 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns TRUE if all values (other than "ID") are
|
|
|
|
* considered empty (by weak boolean comparison).
|
|
|
|
*
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
public function isEmpty()
|
|
|
|
{
|
2017-02-22 04:14:53 +01:00
|
|
|
$fixed = DataObject::config()->uninherited('fixed_fields');
|
2016-11-29 00:31:16 +01:00
|
|
|
foreach ($this->toMap() as $field => $value) {
|
|
|
|
// only look at custom fields
|
|
|
|
if (isset($fixed[$field])) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$dbObject = $this->dbObject($field);
|
|
|
|
if (!$dbObject) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if ($dbObject->exists()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pluralise this item given a specific count.
|
|
|
|
*
|
|
|
|
* E.g. "0 Pages", "1 File", "3 Images"
|
|
|
|
*
|
|
|
|
* @param string $count
|
|
|
|
* @return string
|
|
|
|
*/
|
2017-01-18 04:58:48 +01:00
|
|
|
public function i18n_pluralise($count)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2017-01-18 04:58:48 +01:00
|
|
|
$default = 'one ' . $this->i18n_singular_name() . '|{count} ' . $this->i18n_plural_name();
|
2017-03-20 22:49:30 +01:00
|
|
|
return i18n::_t(
|
2018-01-16 19:39:30 +01:00
|
|
|
static::class . '.PLURALS',
|
2017-01-18 04:58:48 +01:00
|
|
|
$default,
|
2018-01-30 06:28:28 +01:00
|
|
|
['count' => $count]
|
2016-11-29 00:31:16 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the user friendly singular name of this DataObject.
|
|
|
|
* If the name is not defined (by redefining $singular_name in the subclass),
|
|
|
|
* this returns the class name.
|
|
|
|
*
|
|
|
|
* @return string User friendly singular name of this DataObject
|
|
|
|
*/
|
|
|
|
public function singular_name()
|
|
|
|
{
|
2017-08-22 23:42:10 +02:00
|
|
|
$name = $this->config()->get('singular_name');
|
2017-01-18 04:58:48 +01:00
|
|
|
if ($name) {
|
|
|
|
return $name;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
2017-01-18 04:58:48 +01:00
|
|
|
return ucwords(trim(strtolower(preg_replace(
|
|
|
|
'/_?([A-Z])/',
|
|
|
|
' $1',
|
|
|
|
ClassInfo::shortName($this)
|
|
|
|
))));
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the translated user friendly singular name of this DataObject
|
|
|
|
* same as singular_name() but runs it through the translating function
|
|
|
|
*
|
|
|
|
* Translating string is in the form:
|
|
|
|
* $this->class.SINGULARNAME
|
|
|
|
* Example:
|
|
|
|
* Page.SINGULARNAME
|
|
|
|
*
|
|
|
|
* @return string User friendly translated singular name of this DataObject
|
|
|
|
*/
|
|
|
|
public function i18n_singular_name()
|
|
|
|
{
|
2018-01-16 19:39:30 +01:00
|
|
|
return _t(static::class . '.SINGULARNAME', $this->singular_name());
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the user friendly plural name of this DataObject
|
|
|
|
* If the name is not defined (by renaming $plural_name in the subclass),
|
|
|
|
* this returns a pluralised version of the class name.
|
|
|
|
*
|
|
|
|
* @return string User friendly plural name of this DataObject
|
|
|
|
*/
|
|
|
|
public function plural_name()
|
|
|
|
{
|
2017-08-22 23:42:10 +02:00
|
|
|
if ($name = $this->config()->get('plural_name')) {
|
2016-11-29 00:31:16 +01:00
|
|
|
return $name;
|
|
|
|
}
|
2017-01-18 04:58:48 +01:00
|
|
|
$name = $this->singular_name();
|
|
|
|
//if the penultimate character is not a vowel, replace "y" with "ies"
|
|
|
|
if (preg_match('/[^aeiou]y$/i', $name)) {
|
|
|
|
$name = substr($name, 0, -1) . 'ie';
|
|
|
|
}
|
|
|
|
return ucfirst($name . 's');
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the translated user friendly plural name of this DataObject
|
|
|
|
* Same as plural_name but runs it through the translation function
|
|
|
|
* Translation string is in the form:
|
|
|
|
* $this->class.PLURALNAME
|
|
|
|
* Example:
|
|
|
|
* Page.PLURALNAME
|
|
|
|
*
|
|
|
|
* @return string User friendly translated plural name of this DataObject
|
|
|
|
*/
|
|
|
|
public function i18n_plural_name()
|
|
|
|
{
|
2018-01-16 19:39:30 +01:00
|
|
|
return _t(static::class . '.PLURALNAME', $this->plural_name());
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Standard implementation of a title/label for a specific
|
|
|
|
* record. Tries to find properties 'Title' or 'Name',
|
|
|
|
* and falls back to the 'ID'. Useful to provide
|
|
|
|
* user-friendly identification of a record, e.g. in errormessages
|
|
|
|
* or UI-selections.
|
|
|
|
*
|
|
|
|
* Overload this method to have a more specialized implementation,
|
|
|
|
* e.g. for an Address record this could be:
|
|
|
|
* <code>
|
|
|
|
* function getTitle() {
|
|
|
|
* return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
|
|
|
|
* }
|
|
|
|
* </code>
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getTitle()
|
|
|
|
{
|
|
|
|
$schema = static::getSchema();
|
|
|
|
if ($schema->fieldSpec($this, 'Title')) {
|
|
|
|
return $this->getField('Title');
|
|
|
|
}
|
|
|
|
if ($schema->fieldSpec($this, 'Name')) {
|
|
|
|
return $this->getField('Name');
|
|
|
|
}
|
|
|
|
|
|
|
|
return "#{$this->ID}";
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the associated database record - in this case, the object itself.
|
|
|
|
* This is included so that you can call $dataOrController->data() and get a DataObject all the time.
|
|
|
|
*
|
|
|
|
* @return DataObject Associated database record
|
|
|
|
*/
|
|
|
|
public function data()
|
|
|
|
{
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert this object to a map.
|
2020-08-20 02:28:31 +02:00
|
|
|
* Note that it has the following quirks:
|
|
|
|
* - custom getters, including those that adjust the result of database fields, won't be executed
|
|
|
|
* - NULL values won't be returned.
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
|
|
|
* @return array The data as a map.
|
|
|
|
*/
|
|
|
|
public function toMap()
|
|
|
|
{
|
|
|
|
$this->loadLazyFields();
|
2020-08-20 02:28:31 +02:00
|
|
|
return array_filter($this->record, function ($val) {
|
|
|
|
return $val !== null;
|
|
|
|
});
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return all currently fetched database fields.
|
|
|
|
*
|
|
|
|
* This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
|
|
|
|
* Obviously, this makes it a lot faster.
|
|
|
|
*
|
|
|
|
* @return array The data as a map.
|
|
|
|
*/
|
|
|
|
public function getQueriedDatabaseFields()
|
|
|
|
{
|
|
|
|
return $this->record;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update a number of fields on this object, given a map of the desired changes.
|
|
|
|
*
|
|
|
|
* The field names can be simple names, or you can use a dot syntax to access $has_one relations.
|
|
|
|
* For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
|
|
|
|
*
|
2019-02-04 08:47:27 +01:00
|
|
|
* Doesn't write the main object, but if you use the dot syntax, it will write()
|
2016-11-29 00:31:16 +01:00
|
|
|
* the related objects that it alters.
|
|
|
|
*
|
2019-02-04 08:47:27 +01:00
|
|
|
* When using this method with user supplied data, it's very important to
|
|
|
|
* whitelist the allowed keys.
|
|
|
|
*
|
2016-11-29 00:31:16 +01:00
|
|
|
* @param array $data A map of field name to data values to update.
|
|
|
|
* @return DataObject $this
|
|
|
|
*/
|
|
|
|
public function update($data)
|
|
|
|
{
|
2017-05-17 07:40:13 +02:00
|
|
|
foreach ($data as $key => $value) {
|
2016-11-29 00:31:16 +01:00
|
|
|
// Implement dot syntax for updates
|
2017-05-17 07:40:13 +02:00
|
|
|
if (strpos($key, '.') !== false) {
|
|
|
|
$relations = explode('.', $key);
|
2016-11-29 00:31:16 +01:00
|
|
|
$fieldName = array_pop($relations);
|
2017-05-17 07:40:13 +02:00
|
|
|
/** @var static $relObj */
|
2016-11-29 00:31:16 +01:00
|
|
|
$relObj = $this;
|
|
|
|
$relation = null;
|
|
|
|
foreach ($relations as $i => $relation) {
|
|
|
|
// no support for has_many or many_many relationships,
|
|
|
|
// as the updater wouldn't know which object to write to (or create)
|
|
|
|
if ($relObj->$relation() instanceof DataObject) {
|
|
|
|
$parentObj = $relObj;
|
|
|
|
$relObj = $relObj->$relation();
|
2017-08-15 01:12:39 +02:00
|
|
|
// If the intermediate relationship objects haven't been created, then write them
|
2018-01-30 06:28:28 +01:00
|
|
|
if ($i < sizeof($relations) - 1 && !$relObj->ID || (!$relObj->ID && $parentObj !== $this)) {
|
2016-11-29 00:31:16 +01:00
|
|
|
$relObj->write();
|
2018-01-16 19:39:30 +01:00
|
|
|
$relatedFieldName = $relation . "ID";
|
2016-11-29 00:31:16 +01:00
|
|
|
$parentObj->$relatedFieldName = $relObj->ID;
|
|
|
|
$parentObj->write();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
user_error(
|
|
|
|
"DataObject::update(): Can't traverse relationship '$relation'," .
|
|
|
|
"it has to be a has_one relationship or return a single DataObject",
|
|
|
|
E_USER_NOTICE
|
|
|
|
);
|
|
|
|
// unset relation object so we don't write properties to the wrong object
|
|
|
|
$relObj = null;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($relObj) {
|
2017-05-17 07:40:13 +02:00
|
|
|
$relObj->$fieldName = $value;
|
2016-11-29 00:31:16 +01:00
|
|
|
$relObj->write();
|
2018-01-16 19:39:30 +01:00
|
|
|
$relatedFieldName = $relation . "ID";
|
2016-11-29 00:31:16 +01:00
|
|
|
$this->$relatedFieldName = $relObj->ID;
|
|
|
|
$relObj->flushCache();
|
|
|
|
} else {
|
2017-05-17 07:40:13 +02:00
|
|
|
$class = static::class;
|
|
|
|
user_error("Couldn't follow dot syntax '{$key}' on '{$class}' object", E_USER_WARNING);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
} else {
|
2017-05-17 07:40:13 +02:00
|
|
|
$this->$key = $value;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pass changes as a map, and try to
|
|
|
|
* get automatic casting for these fields.
|
|
|
|
* Doesn't write to the database. To write the data,
|
|
|
|
* use the write() method.
|
|
|
|
*
|
|
|
|
* @param array $data A map of field name to data values to update.
|
|
|
|
* @return DataObject $this
|
|
|
|
*/
|
|
|
|
public function castedUpdate($data)
|
|
|
|
{
|
|
|
|
foreach ($data as $k => $v) {
|
|
|
|
$this->setCastedField($k, $v);
|
|
|
|
}
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Merges data and relations from another object of same class,
|
|
|
|
* without conflict resolution. Allows to specify which
|
|
|
|
* dataset takes priority in case its not empty.
|
|
|
|
* has_one-relations are just transferred with priority 'right'.
|
|
|
|
* has_many and many_many-relations are added regardless of priority.
|
|
|
|
*
|
|
|
|
* Caution: has_many/many_many relations are moved rather than duplicated,
|
|
|
|
* meaning they are not connected to the merged object any longer.
|
|
|
|
* Caution: Just saves updated has_many/many_many relations to the database,
|
|
|
|
* doesn't write the updated object itself (just writes the object-properties).
|
|
|
|
* Caution: Does not delete the merged object.
|
|
|
|
* Caution: Does now overwrite Created date on the original object.
|
|
|
|
*
|
|
|
|
* @param DataObject $rightObj
|
|
|
|
* @param string $priority left|right Determines who wins in case of a conflict (optional)
|
|
|
|
* @param bool $includeRelations Merge any existing relations (optional)
|
|
|
|
* @param bool $overwriteWithEmpty Overwrite existing left values with empty right values.
|
|
|
|
* Only applicable with $priority='right'. (optional)
|
|
|
|
* @return Boolean
|
|
|
|
*/
|
|
|
|
public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false)
|
|
|
|
{
|
|
|
|
$leftObj = $this;
|
|
|
|
|
|
|
|
if ($leftObj->ClassName != $rightObj->ClassName) {
|
|
|
|
// we can't merge similiar subclasses because they might have additional relations
|
|
|
|
user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
|
2008-08-09 07:04:15 +02:00
|
|
|
(expected '{$leftObj->ClassName}').", E_USER_WARNING);
|
2016-11-29 00:31:16 +01:00
|
|
|
return false;
|
|
|
|
}
|
2007-10-18 22:04:02 +02:00
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
if (!$rightObj->ID) {
|
|
|
|
user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
|
2007-10-18 22:04:02 +02:00
|
|
|
to make sure all relations are transferred properly.').", E_USER_WARNING);
|
2016-11-29 00:31:16 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// makes sure we don't merge data like ID or ClassName
|
|
|
|
$rightData = DataObject::getSchema()->fieldSpecs(get_class($rightObj));
|
|
|
|
foreach ($rightData as $key => $rightSpec) {
|
|
|
|
// Don't merge ID
|
|
|
|
if ($key === 'ID') {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only merge relations if allowed
|
|
|
|
if ($rightSpec === 'ForeignKey' && !$includeRelations) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// don't merge conflicting values if priority is 'left'
|
|
|
|
if ($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
|
|
|
|
if ($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO remove redundant merge of has_one fields
|
|
|
|
$leftObj->{$key} = $rightObj->{$key};
|
|
|
|
}
|
|
|
|
|
|
|
|
// merge relations
|
|
|
|
if ($includeRelations) {
|
|
|
|
if ($manyMany = $this->manyMany()) {
|
|
|
|
foreach ($manyMany as $relationship => $class) {
|
2017-05-17 07:40:13 +02:00
|
|
|
/** @var DataObject $leftComponents */
|
2016-11-29 00:31:16 +01:00
|
|
|
$leftComponents = $leftObj->getManyManyComponents($relationship);
|
|
|
|
$rightComponents = $rightObj->getManyManyComponents($relationship);
|
|
|
|
if ($rightComponents && $rightComponents->exists()) {
|
|
|
|
$leftComponents->addMany($rightComponents->column('ID'));
|
|
|
|
}
|
|
|
|
$leftComponents->write();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($hasMany = $this->hasMany()) {
|
|
|
|
foreach ($hasMany as $relationship => $class) {
|
|
|
|
$leftComponents = $leftObj->getComponents($relationship);
|
|
|
|
$rightComponents = $rightObj->getComponents($relationship);
|
|
|
|
if ($rightComponents && $rightComponents->exists()) {
|
|
|
|
$leftComponents->addMany($rightComponents->column('ID'));
|
|
|
|
}
|
|
|
|
$leftComponents->write();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Forces the record to think that all its data has changed.
|
2018-10-08 03:55:02 +02:00
|
|
|
* Doesn't write to the database. Force-change preseved until
|
|
|
|
* next write. Existing CHANGE_VALUE or CHANGE_STRICT values
|
|
|
|
* are preserved.
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function forceChange()
|
|
|
|
{
|
|
|
|
// Ensure lazy fields loaded
|
|
|
|
$this->loadLazyFields();
|
|
|
|
|
2018-10-08 03:55:02 +02:00
|
|
|
// Populate the null values in record so that they actually get written
|
|
|
|
foreach (array_keys(static::getSchema()->fieldSpecs(static::class)) as $fieldName) {
|
2016-11-29 00:31:16 +01:00
|
|
|
if (!isset($this->record[$fieldName])) {
|
|
|
|
$this->record[$fieldName] = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-08 03:55:02 +02:00
|
|
|
$this->changeForced = true;
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validate the current object.
|
|
|
|
*
|
|
|
|
* By default, there is no validation - objects are always valid! However, you can overload this method in your
|
|
|
|
* DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
|
|
|
|
*
|
|
|
|
* Invalid objects won't be able to be written - a warning will be thrown and no write will occur. onBeforeWrite()
|
|
|
|
* and onAfterWrite() won't get called either.
|
|
|
|
*
|
|
|
|
* It is expected that you call validate() in your own application to test that an object is valid before
|
|
|
|
* attempting a write, and respond appropriately if it isn't.
|
|
|
|
*
|
|
|
|
* @see {@link ValidationResult}
|
|
|
|
* @return ValidationResult
|
|
|
|
*/
|
|
|
|
public function validate()
|
|
|
|
{
|
|
|
|
$result = ValidationResult::create();
|
|
|
|
$this->extend('validate', $result);
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Public accessor for {@see DataObject::validate()}
|
|
|
|
*
|
|
|
|
* @return ValidationResult
|
|
|
|
*/
|
|
|
|
public function doValidate()
|
|
|
|
{
|
|
|
|
Deprecation::notice('5.0', 'Use validate');
|
|
|
|
return $this->validate();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Event handler called before writing to the database.
|
|
|
|
* You can overload this to clean up or otherwise process data before writing it to the
|
|
|
|
* database. Don't forget to call parent::onBeforeWrite(), though!
|
|
|
|
*
|
|
|
|
* This called after {@link $this->validate()}, so you can be sure that your data is valid.
|
|
|
|
*
|
2020-12-21 22:23:23 +01:00
|
|
|
* @uses DataExtension::onBeforeWrite()
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
protected function onBeforeWrite()
|
|
|
|
{
|
|
|
|
$this->brokenOnWrite = false;
|
|
|
|
|
|
|
|
$dummy = null;
|
|
|
|
$this->extend('onBeforeWrite', $dummy);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Event handler called after writing to the database.
|
|
|
|
* You can overload this to act upon changes made to the data after it is written.
|
|
|
|
* $this->changed will have a record
|
|
|
|
* database. Don't forget to call parent::onAfterWrite(), though!
|
|
|
|
*
|
2020-12-21 22:23:23 +01:00
|
|
|
* @uses DataExtension::onAfterWrite()
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
protected function onAfterWrite()
|
|
|
|
{
|
|
|
|
$dummy = null;
|
|
|
|
$this->extend('onAfterWrite', $dummy);
|
|
|
|
}
|
|
|
|
|
2017-08-08 06:46:34 +02:00
|
|
|
/**
|
|
|
|
* Find all objects that will be cascade deleted if this object is deleted
|
|
|
|
*
|
|
|
|
* Notes:
|
|
|
|
* - If this object is versioned, objects will only be searched in the same stage as the given record.
|
|
|
|
* - This will only be useful prior to deletion, as post-deletion this record will no longer exist.
|
|
|
|
*
|
|
|
|
* @param bool $recursive True if recursive
|
|
|
|
* @param ArrayList $list Optional list to add items to
|
|
|
|
* @return ArrayList list of objects
|
|
|
|
*/
|
|
|
|
public function findCascadeDeletes($recursive = true, $list = null)
|
|
|
|
{
|
|
|
|
// Find objects in these relationships
|
|
|
|
return $this->findRelatedObjects('cascade_deletes', $recursive, $list);
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* Event handler called before deleting from the database.
|
|
|
|
* You can overload this to clean up or otherwise process data before delete this
|
|
|
|
* record. Don't forget to call parent::onBeforeDelete(), though!
|
|
|
|
*
|
2020-12-21 22:23:23 +01:00
|
|
|
* @uses DataExtension::onBeforeDelete()
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
protected function onBeforeDelete()
|
|
|
|
{
|
|
|
|
$this->brokenOnDelete = false;
|
|
|
|
|
|
|
|
$dummy = null;
|
|
|
|
$this->extend('onBeforeDelete', $dummy);
|
2017-08-08 06:46:34 +02:00
|
|
|
|
|
|
|
// Cascade deletes
|
|
|
|
$deletes = $this->findCascadeDeletes(false);
|
|
|
|
foreach ($deletes as $delete) {
|
|
|
|
$delete->delete();
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
protected function onAfterDelete()
|
|
|
|
{
|
|
|
|
$this->extend('onAfterDelete');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load the default values in from the self::$defaults array.
|
|
|
|
* Will traverse the defaults of the current class and all its parent classes.
|
|
|
|
* Called by the constructor when creating new records.
|
|
|
|
*
|
2020-12-21 22:23:23 +01:00
|
|
|
* @uses DataExtension::populateDefaults()
|
2016-11-29 00:31:16 +01:00
|
|
|
* @return DataObject $this
|
|
|
|
*/
|
|
|
|
public function populateDefaults()
|
|
|
|
{
|
|
|
|
$classes = array_reverse(ClassInfo::ancestry($this));
|
|
|
|
|
|
|
|
foreach ($classes as $class) {
|
|
|
|
$defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
|
|
|
|
|
|
|
|
if ($defaults && !is_array($defaults)) {
|
|
|
|
user_error(
|
2017-05-17 07:40:13 +02:00
|
|
|
"Bad '" . static::class . "' defaults given: " . var_export($defaults, true),
|
2016-11-29 00:31:16 +01:00
|
|
|
E_USER_WARNING
|
|
|
|
);
|
|
|
|
$defaults = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($defaults) {
|
|
|
|
foreach ($defaults as $fieldName => $fieldValue) {
|
2018-01-30 06:28:28 +01:00
|
|
|
// SRM 2007-03-06: Stricter check
|
2016-11-29 00:31:16 +01:00
|
|
|
if (!isset($this->$fieldName) || $this->$fieldName === null) {
|
|
|
|
$this->$fieldName = $fieldValue;
|
|
|
|
}
|
2018-01-30 06:28:28 +01:00
|
|
|
// Set many-many defaults with an array of ids
|
2016-11-29 00:31:16 +01:00
|
|
|
if (is_array($fieldValue) && $this->getSchema()->manyManyComponent(static::class, $fieldName)) {
|
|
|
|
/** @var ManyManyList $manyManyJoin */
|
|
|
|
$manyManyJoin = $this->$fieldName();
|
|
|
|
$manyManyJoin->setByIDList($fieldValue);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($class == self::class) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->extend('populateDefaults');
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine validation of this object prior to write
|
|
|
|
*
|
|
|
|
* @return ValidationException Exception generated by this write, or null if valid
|
|
|
|
*/
|
|
|
|
protected function validateWrite()
|
|
|
|
{
|
|
|
|
if ($this->ObsoleteClassName) {
|
|
|
|
return new ValidationException(
|
2018-01-16 19:39:30 +01:00
|
|
|
"Object is of class '{$this->ObsoleteClassName}' which doesn't exist - " .
|
2016-11-23 06:09:10 +01:00
|
|
|
"you need to change the ClassName before you can write it"
|
2016-11-29 00:31:16 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-02-22 04:14:53 +01:00
|
|
|
// Note: Validation can only be disabled at the global level, not per-model
|
|
|
|
if (DataObject::config()->uninherited('validation_enabled')) {
|
2016-11-29 00:31:16 +01:00
|
|
|
$result = $this->validate();
|
2016-11-23 06:09:10 +01:00
|
|
|
if (!$result->isValid()) {
|
|
|
|
return new ValidationException($result);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Prepare an object prior to write
|
|
|
|
*
|
|
|
|
* @throws ValidationException
|
|
|
|
*/
|
|
|
|
protected function preWrite()
|
|
|
|
{
|
|
|
|
// Validate this object
|
|
|
|
if ($writeException = $this->validateWrite()) {
|
|
|
|
// Used by DODs to clean up after themselves, eg, Versioned
|
|
|
|
$this->invokeWithExtensions('onAfterSkippedWrite');
|
|
|
|
throw $writeException;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check onBeforeWrite
|
|
|
|
$this->brokenOnWrite = true;
|
|
|
|
$this->onBeforeWrite();
|
|
|
|
if ($this->brokenOnWrite) {
|
2020-09-25 02:09:37 +02:00
|
|
|
throw new LogicException(
|
|
|
|
static::class . " has a broken onBeforeWrite() function."
|
|
|
|
. " Make sure that you call parent::onBeforeWrite()."
|
|
|
|
);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Detects and updates all changes made to this object
|
|
|
|
*
|
|
|
|
* @param bool $forceChanges If set to true, force all fields to be treated as changed
|
|
|
|
* @return bool True if any changes are detected
|
|
|
|
*/
|
|
|
|
protected function updateChanges($forceChanges = false)
|
|
|
|
{
|
|
|
|
if ($forceChanges) {
|
|
|
|
// Force changes, but only for loaded fields
|
|
|
|
foreach ($this->record as $field => $value) {
|
|
|
|
$this->changed[$field] = static::CHANGE_VALUE;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return $this->isChanged();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Writes a subset of changes for a specific table to the given manipulation
|
|
|
|
*
|
|
|
|
* @param string $baseTable Base table
|
|
|
|
* @param string $now Timestamp to use for the current time
|
|
|
|
* @param bool $isNewRecord Whether this should be treated as a new record write
|
|
|
|
* @param array $manipulation Manipulation to write to
|
|
|
|
* @param string $class Class of table to manipulate
|
|
|
|
*/
|
|
|
|
protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class)
|
|
|
|
{
|
|
|
|
$schema = $this->getSchema();
|
|
|
|
$table = $schema->tableName($class);
|
2020-04-20 19:58:09 +02:00
|
|
|
$manipulation[$table] = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2018-10-08 03:55:02 +02:00
|
|
|
$changed = $this->getChangedFields();
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
// Extract records for this table
|
|
|
|
foreach ($this->record as $fieldName => $fieldValue) {
|
|
|
|
// we're not attempting to reset the BaseTable->ID
|
|
|
|
// Ignore unchanged fields or attempts to reset the BaseTable->ID
|
2018-10-08 03:55:02 +02:00
|
|
|
if (empty($changed[$fieldName]) || ($table === $baseTable && $fieldName === 'ID')) {
|
2016-11-29 00:31:16 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure this field pertains to this table
|
2018-01-30 06:28:28 +01:00
|
|
|
$specification = $schema->fieldSpec(
|
|
|
|
$class,
|
|
|
|
$fieldName,
|
|
|
|
DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED
|
|
|
|
);
|
2016-11-29 00:31:16 +01:00
|
|
|
if (!$specification) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if database column doesn't correlate to a DBField instance...
|
|
|
|
$fieldObj = $this->dbObject($fieldName);
|
|
|
|
if (!$fieldObj) {
|
|
|
|
$fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write to manipulation
|
|
|
|
$fieldObj->writeToManipulation($manipulation[$table]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure update of Created and LastEdited columns
|
|
|
|
if ($baseTable === $table) {
|
|
|
|
$manipulation[$table]['fields']['LastEdited'] = $now;
|
|
|
|
if ($isNewRecord) {
|
2018-01-30 06:28:28 +01:00
|
|
|
$manipulation[$table]['fields']['Created'] = empty($this->record['Created'])
|
|
|
|
? $now
|
|
|
|
: $this->record['Created'];
|
2017-05-17 07:40:13 +02:00
|
|
|
$manipulation[$table]['fields']['ClassName'] = static::class;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Inserts done one the base table are performed in another step, so the manipulation should instead
|
|
|
|
// attempt an update, as though it were a normal update.
|
|
|
|
$manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
|
|
|
|
$manipulation[$table]['class'] = $class;
|
2019-02-27 03:12:26 +01:00
|
|
|
if ($this->isInDB()) {
|
|
|
|
$manipulation[$table]['id'] = $this->record['ID'];
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Ensures that a blank base record exists with the basic fixed fields for this dataobject
|
|
|
|
*
|
|
|
|
* Does nothing if an ID is already assigned for this record
|
|
|
|
*
|
|
|
|
* @param string $baseTable Base table
|
|
|
|
* @param string $now Timestamp to use for the current time
|
|
|
|
*/
|
|
|
|
protected function writeBaseRecord($baseTable, $now)
|
|
|
|
{
|
|
|
|
// Generate new ID if not specified
|
|
|
|
if ($this->isInDB()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Perform an insert on the base table
|
2019-02-27 03:12:26 +01:00
|
|
|
$manipulation = [];
|
|
|
|
$this->prepareManipulationTable($baseTable, $now, true, $manipulation, $this->baseClass());
|
|
|
|
DB::manipulate($manipulation);
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
$this->changed['ID'] = self::CHANGE_VALUE;
|
|
|
|
$this->record['ID'] = DB::get_generated_id($baseTable);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate and write the database manipulation for all changed fields
|
|
|
|
*
|
|
|
|
* @param string $baseTable Base table
|
|
|
|
* @param string $now Timestamp to use for the current time
|
|
|
|
* @param bool $isNewRecord If this is a new record
|
2018-12-18 04:46:56 +01:00
|
|
|
* @throws InvalidArgumentException
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
protected function writeManipulation($baseTable, $now, $isNewRecord)
|
|
|
|
{
|
|
|
|
// Generate database manipulations for each class
|
2020-04-20 19:58:09 +02:00
|
|
|
$manipulation = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
foreach (ClassInfo::ancestry(static::class, true) as $class) {
|
|
|
|
$this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Allow extensions to extend this manipulation
|
|
|
|
$this->extend('augmentWrite', $manipulation);
|
|
|
|
|
|
|
|
// New records have their insert into the base data table done first, so that they can pass the
|
|
|
|
// generated ID on to the rest of the manipulation
|
|
|
|
if ($isNewRecord) {
|
|
|
|
$manipulation[$baseTable]['command'] = 'update';
|
|
|
|
}
|
|
|
|
|
2018-12-18 04:46:56 +01:00
|
|
|
// Make sure none of our field assignment are arrays
|
|
|
|
foreach ($manipulation as $tableManipulation) {
|
|
|
|
if (!isset($tableManipulation['fields'])) {
|
|
|
|
continue;
|
|
|
|
}
|
2019-02-21 23:08:43 +01:00
|
|
|
foreach ($tableManipulation['fields'] as $fieldName => $fieldValue) {
|
2018-12-18 04:46:56 +01:00
|
|
|
if (is_array($fieldValue)) {
|
2019-02-21 23:08:43 +01:00
|
|
|
$dbObject = $this->dbObject($fieldName);
|
|
|
|
// If the field allows non-scalar values we'll let it do dynamic assignments
|
|
|
|
if ($dbObject && $dbObject->scalarValueOnly()) {
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
'DataObject::writeManipulation: parameterised field assignments are disallowed'
|
|
|
|
);
|
|
|
|
}
|
2018-12-18 04:46:56 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
// Perform the manipulation
|
|
|
|
DB::manipulate($manipulation);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Writes all changes to this object to the database.
|
|
|
|
* - It will insert a record whenever ID isn't set, otherwise update.
|
|
|
|
* - All relevant tables will be updated.
|
|
|
|
* - $this->onBeforeWrite() gets called beforehand.
|
|
|
|
* - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
|
|
|
|
*
|
2020-12-21 22:23:23 +01:00
|
|
|
* @uses DataExtension::augmentWrite()
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
2019-09-10 04:15:28 +02:00
|
|
|
* @param boolean $showDebug Show debugging information
|
|
|
|
* @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
|
|
|
|
* @param boolean $forceWrite Write to database even if there are no changes
|
|
|
|
* @param boolean|array $writeComponents Call write() on all associated component instances which were previously
|
|
|
|
* retrieved through {@link getComponent()}, {@link getComponents()} or
|
|
|
|
* {@link getManyManyComponents()}. Default to `false`. The parameter can also be provided in
|
|
|
|
* the form of an array: `['recursive' => true, skip => ['Page'=>[1,2,3]]`. This avoid infinite
|
|
|
|
* loops when one DataObject are components of each other.
|
2016-11-29 00:31:16 +01:00
|
|
|
* @return int The ID of the record
|
|
|
|
* @throws ValidationException Exception that can be caught and handled by the calling function
|
|
|
|
*/
|
|
|
|
public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false)
|
|
|
|
{
|
|
|
|
$now = DBDatetime::now()->Rfc2822();
|
|
|
|
|
|
|
|
// Execute pre-write tasks
|
|
|
|
$this->preWrite();
|
|
|
|
|
|
|
|
// Check if we are doing an update or an insert
|
|
|
|
$isNewRecord = !$this->isInDB() || $forceInsert;
|
|
|
|
|
|
|
|
// Check changes exist, abort if there are none
|
|
|
|
$hasChanges = $this->updateChanges($isNewRecord);
|
|
|
|
if ($hasChanges || $forceWrite || $isNewRecord) {
|
2017-08-17 16:02:25 +02:00
|
|
|
// Ensure Created and LastEdited are populated
|
|
|
|
if (!isset($this->record['Created'])) {
|
|
|
|
$this->record['Created'] = $now;
|
|
|
|
}
|
|
|
|
$this->record['LastEdited'] = $now;
|
2017-08-22 23:42:10 +02:00
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
// New records have their insert into the base data table done first, so that they can pass the
|
|
|
|
// generated primary key on to the rest of the manipulation
|
|
|
|
$baseTable = $this->baseTable();
|
|
|
|
$this->writeBaseRecord($baseTable, $now);
|
|
|
|
|
|
|
|
// Write the DB manipulation for all changed fields
|
|
|
|
$this->writeManipulation($baseTable, $now, $isNewRecord);
|
|
|
|
|
|
|
|
// If there's any relations that couldn't be saved before, save them now (we have an ID here)
|
|
|
|
$this->writeRelations();
|
|
|
|
$this->onAfterWrite();
|
2018-10-05 00:09:00 +02:00
|
|
|
|
|
|
|
// Reset isChanged data
|
|
|
|
// DBComposites properly bound to the parent record will also have their isChanged value reset
|
|
|
|
$this->changed = [];
|
2018-10-08 03:55:02 +02:00
|
|
|
$this->changeForced = false;
|
2018-10-05 00:09:00 +02:00
|
|
|
$this->original = $this->record;
|
2016-11-29 00:31:16 +01:00
|
|
|
} else {
|
|
|
|
if ($showDebug) {
|
|
|
|
Debug::message("no changes for DataObject");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Used by DODs to clean up after themselves, eg, Versioned
|
|
|
|
$this->invokeWithExtensions('onAfterSkippedWrite');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write relations as necessary
|
|
|
|
if ($writeComponents) {
|
2019-09-10 04:15:28 +02:00
|
|
|
$recursive = true;
|
|
|
|
$skip = [];
|
|
|
|
if (is_array($writeComponents)) {
|
|
|
|
$recursive = isset($writeComponents['recursive']) && $writeComponents['recursive'];
|
|
|
|
$skip = isset($writeComponents['skip']) && is_array($writeComponents['skip'])
|
|
|
|
? $writeComponents['skip']
|
|
|
|
: [];
|
|
|
|
}
|
|
|
|
$this->writeComponents($recursive, $skip);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Clears the cache for this object so get_one returns the correct object.
|
|
|
|
$this->flushCache();
|
|
|
|
|
|
|
|
return $this->record['ID'];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Writes cached relation lists to the database, if possible
|
|
|
|
*/
|
|
|
|
public function writeRelations()
|
|
|
|
{
|
|
|
|
if (!$this->isInDB()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there's any relations that couldn't be saved before, save them now (we have an ID here)
|
|
|
|
if ($this->unsavedRelations) {
|
|
|
|
foreach ($this->unsavedRelations as $name => $list) {
|
|
|
|
$list->changeToList($this->$name());
|
|
|
|
}
|
2020-04-20 19:58:09 +02:00
|
|
|
$this->unsavedRelations = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Write the cached components to the database. Cached components could refer to two different instances of the
|
|
|
|
* same record.
|
|
|
|
*
|
|
|
|
* @param bool $recursive Recursively write components
|
2019-09-10 04:15:28 +02:00
|
|
|
* @param array $skip List of DataObject references to skip
|
2016-11-29 00:31:16 +01:00
|
|
|
* @return DataObject $this
|
|
|
|
*/
|
2019-09-10 04:15:28 +02:00
|
|
|
public function writeComponents($recursive = false, $skip = [])
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2019-09-10 04:15:28 +02:00
|
|
|
// Make sure we add our current object to the skip list
|
|
|
|
$this->skipWriteComponents($recursive, $this, $skip);
|
|
|
|
|
|
|
|
// All our write calls have the same arguments ... just need make sure the skip list is pass by reference
|
|
|
|
$args = [
|
|
|
|
false, false, false,
|
|
|
|
$recursive ? ["recursive" => $recursive, "skip" => &$skip] : false
|
|
|
|
];
|
|
|
|
|
2018-01-30 06:28:28 +01:00
|
|
|
foreach ($this->components as $component) {
|
2019-09-10 04:15:28 +02:00
|
|
|
if (!$this->skipWriteComponents($recursive, $component, $skip)) {
|
|
|
|
$component->write(...$args);
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($join = $this->getJoin()) {
|
2019-09-10 04:15:28 +02:00
|
|
|
if (!$this->skipWriteComponents($recursive, $join, $skip)) {
|
|
|
|
$join->write(...$args);
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2019-09-10 04:15:28 +02:00
|
|
|
/**
|
|
|
|
* Check if target is in the skip list and add it if it isn't.
|
|
|
|
* @param bool $recursive
|
|
|
|
* @param DataObject $target
|
|
|
|
* @param array $skip
|
|
|
|
* @return bool Whether the target is already in the list
|
|
|
|
*/
|
|
|
|
private function skipWriteComponents($recursive, DataObject $target, array &$skip)
|
|
|
|
{
|
2019-11-26 21:32:33 +01:00
|
|
|
// skip writing component if it doesn't exist
|
|
|
|
if (!$target->exists()) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-09-10 04:15:28 +02:00
|
|
|
// We only care about the skip list if our call is meant to be recursive
|
|
|
|
if (!$recursive) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get our Skip array keys
|
|
|
|
$classname = get_class($target);
|
|
|
|
$id = $target->ID;
|
|
|
|
|
|
|
|
// Check if the target is in the skip list
|
|
|
|
if (isset($skip[$classname])) {
|
|
|
|
if (in_array($id, $skip[$classname])) {
|
|
|
|
// Skip the object
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// This is the first object of this class
|
|
|
|
$skip[$classname] = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add the target to our skip list
|
|
|
|
$skip[$classname][] = $id;
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* Delete this data object.
|
|
|
|
* $this->onBeforeDelete() gets called.
|
|
|
|
* Note that in Versioned objects, both Stage and Live will be deleted.
|
2020-12-21 22:23:23 +01:00
|
|
|
* @uses DataExtension::augmentSQL()
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function delete()
|
|
|
|
{
|
|
|
|
$this->brokenOnDelete = true;
|
|
|
|
$this->onBeforeDelete();
|
|
|
|
if ($this->brokenOnDelete) {
|
2020-09-25 02:09:37 +02:00
|
|
|
throw new LogicException(
|
|
|
|
static::class . " has a broken onBeforeDelete() function."
|
|
|
|
. " Make sure that you call parent::onBeforeDelete()."
|
|
|
|
);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Deleting a record without an ID shouldn't do anything
|
|
|
|
if (!$this->ID) {
|
|
|
|
throw new LogicException("DataObject::delete() called on a DataObject without an ID");
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: This is quite ugly. To improve:
|
|
|
|
// - move the details of the delete code in the DataQuery system
|
|
|
|
// - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
|
|
|
|
// obviously, that means getting requireTable() to configure cascading deletes ;-)
|
2017-06-22 12:50:45 +02:00
|
|
|
$srcQuery = DataList::create(static::class)
|
2017-05-17 07:40:13 +02:00
|
|
|
->filter('ID', $this->ID)
|
|
|
|
->dataQuery()
|
|
|
|
->query();
|
2017-10-12 22:55:00 +02:00
|
|
|
$queriedTables = $srcQuery->queriedTables();
|
|
|
|
$this->extend('updateDeleteTables', $queriedTables, $srcQuery);
|
|
|
|
foreach ($queriedTables as $table) {
|
2020-04-20 19:58:09 +02:00
|
|
|
$delete = SQLDelete::create("\"$table\"", ['"ID"' => $this->ID]);
|
2017-10-12 22:55:00 +02:00
|
|
|
$this->extend('updateDeleteTable', $delete, $table, $queriedTables, $srcQuery);
|
2016-11-29 00:31:16 +01:00
|
|
|
$delete->execute();
|
|
|
|
}
|
|
|
|
// Remove this item out of any caches
|
|
|
|
$this->flushCache();
|
|
|
|
|
|
|
|
$this->onAfterDelete();
|
|
|
|
|
|
|
|
$this->OldID = $this->ID;
|
|
|
|
$this->ID = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete the record with the given ID.
|
|
|
|
*
|
|
|
|
* @param string $className The class name of the record to be deleted
|
|
|
|
* @param int $id ID of record to be deleted
|
|
|
|
*/
|
|
|
|
public static function delete_by_id($className, $id)
|
|
|
|
{
|
|
|
|
$obj = DataObject::get_by_id($className, $id);
|
|
|
|
if ($obj) {
|
|
|
|
$obj->delete();
|
|
|
|
} else {
|
|
|
|
user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the class ancestry, including the current class name.
|
|
|
|
* The ancestry will be returned as an array of class names, where the 0th element
|
|
|
|
* will be the class that inherits directly from DataObject, and the last element
|
|
|
|
* will be the current class.
|
|
|
|
*
|
|
|
|
* @return array Class ancestry
|
|
|
|
*/
|
|
|
|
public function getClassAncestry()
|
|
|
|
{
|
|
|
|
return ClassInfo::ancestry(static::class);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-01-30 06:28:28 +01:00
|
|
|
* Return a unary component object from a one to one relationship, as a DataObject.
|
2016-11-29 00:31:16 +01:00
|
|
|
* If no component is available, an 'empty component' will be returned for
|
|
|
|
* non-polymorphic relations, or for polymorphic relations with a class set.
|
|
|
|
*
|
|
|
|
* @param string $componentName Name of the component
|
|
|
|
* @return DataObject The component object. It's exact type will be that of the component.
|
|
|
|
* @throws Exception
|
|
|
|
*/
|
|
|
|
public function getComponent($componentName)
|
|
|
|
{
|
|
|
|
if (isset($this->components[$componentName])) {
|
|
|
|
return $this->components[$componentName];
|
|
|
|
}
|
|
|
|
|
|
|
|
$schema = static::getSchema();
|
|
|
|
if ($class = $schema->hasOneComponent(static::class, $componentName)) {
|
|
|
|
$joinField = $componentName . 'ID';
|
2018-01-30 06:28:28 +01:00
|
|
|
$joinID = $this->getField($joinField);
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
// Extract class name for polymorphic relations
|
|
|
|
if ($class === self::class) {
|
|
|
|
$class = $this->getField($componentName . 'Class');
|
|
|
|
if (empty($class)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($joinID) {
|
|
|
|
// Ensure that the selected object originates from the same stage, subsite, etc
|
|
|
|
$component = DataObject::get($class)
|
|
|
|
->filter('ID', $joinID)
|
|
|
|
->setDataQueryParam($this->getInheritableQueryParams())
|
|
|
|
->first();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (empty($component)) {
|
2017-06-22 12:50:45 +02:00
|
|
|
$component = Injector::inst()->create($class);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
} elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
|
|
|
|
$joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
|
|
|
|
$joinID = $this->ID;
|
|
|
|
|
|
|
|
if ($joinID) {
|
|
|
|
// Prepare filter for appropriate join type
|
|
|
|
if ($polymorphic) {
|
2020-04-20 19:58:09 +02:00
|
|
|
$filter = [
|
2016-11-29 00:31:16 +01:00
|
|
|
"{$joinField}ID" => $joinID,
|
2017-05-17 07:40:13 +02:00
|
|
|
"{$joinField}Class" => static::class,
|
2020-04-20 19:58:09 +02:00
|
|
|
];
|
2016-11-29 00:31:16 +01:00
|
|
|
} else {
|
2020-04-20 19:58:09 +02:00
|
|
|
$filter = [
|
2016-11-29 00:31:16 +01:00
|
|
|
$joinField => $joinID
|
2020-04-20 19:58:09 +02:00
|
|
|
];
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure that the selected object originates from the same stage, subsite, etc
|
|
|
|
$component = DataObject::get($class)
|
|
|
|
->filter($filter)
|
|
|
|
->setDataQueryParam($this->getInheritableQueryParams())
|
|
|
|
->first();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (empty($component)) {
|
2017-06-22 12:50:45 +02:00
|
|
|
$component = Injector::inst()->create($class);
|
2016-11-29 00:31:16 +01:00
|
|
|
if ($polymorphic) {
|
2018-01-16 19:39:30 +01:00
|
|
|
$component->{$joinField . 'ID'} = $this->ID;
|
|
|
|
$component->{$joinField . 'Class'} = static::class;
|
2016-11-29 00:31:16 +01:00
|
|
|
} else {
|
|
|
|
$component->$joinField = $this->ID;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
"DataObject->getComponent(): Could not find component '$componentName'."
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->components[$componentName] = $component;
|
|
|
|
return $component;
|
|
|
|
}
|
|
|
|
|
2018-01-30 06:28:28 +01:00
|
|
|
/**
|
|
|
|
* Assign an item to the given component
|
|
|
|
*
|
|
|
|
* @param string $componentName
|
|
|
|
* @param DataObject|null $item
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setComponent($componentName, $item)
|
|
|
|
{
|
|
|
|
// Validate component
|
|
|
|
$schema = static::getSchema();
|
|
|
|
if ($class = $schema->hasOneComponent(static::class, $componentName)) {
|
|
|
|
// Force item to be written if not by this point
|
|
|
|
// @todo This could be lazy-written in a beforeWrite hook, but force write here for simplicity
|
|
|
|
// https://github.com/silverstripe/silverstripe-framework/issues/7818
|
|
|
|
if ($item && !$item->isInDB()) {
|
|
|
|
$item->write();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update local ID
|
|
|
|
$joinField = $componentName . 'ID';
|
|
|
|
$this->setField($joinField, $item ? $item->ID : null);
|
|
|
|
// Update Class (Polymorphic has_one)
|
|
|
|
// Extract class name for polymorphic relations
|
|
|
|
if ($class === self::class) {
|
|
|
|
$this->setField($componentName . 'Class', $item ? get_class($item) : null);
|
|
|
|
}
|
|
|
|
} elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
|
|
|
|
if ($item) {
|
|
|
|
// For belongs_to, add to has_one on other component
|
|
|
|
$joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
|
|
|
|
if (!$polymorphic) {
|
|
|
|
$joinField = substr($joinField, 0, -2);
|
|
|
|
}
|
|
|
|
$item->setComponent($joinField, $this);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
"DataObject->setComponent(): Could not find component '$componentName'."
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->components[$componentName] = $item;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* Returns a one-to-many relation as a HasManyList
|
|
|
|
*
|
|
|
|
* @param string $componentName Name of the component
|
2017-11-09 05:08:31 +01:00
|
|
|
* @param int|array $id Optional ID(s) for parent of this relation, if not the current record
|
2016-11-29 00:31:16 +01:00
|
|
|
* @return HasManyList|UnsavedRelationList The components of the one-to-many relationship.
|
|
|
|
*/
|
2017-11-09 05:08:31 +01:00
|
|
|
public function getComponents($componentName, $id = null)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2017-11-09 05:08:31 +01:00
|
|
|
if (!isset($id)) {
|
|
|
|
$id = $this->ID;
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
$result = null;
|
|
|
|
|
|
|
|
$schema = $this->getSchema();
|
|
|
|
$componentClass = $schema->hasManyComponent(static::class, $componentName);
|
|
|
|
if (!$componentClass) {
|
|
|
|
throw new InvalidArgumentException(sprintf(
|
|
|
|
"DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
|
|
|
|
$componentName,
|
2017-05-17 07:40:13 +02:00
|
|
|
static::class
|
2016-11-29 00:31:16 +01:00
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we haven't been written yet, we can't save these relations, so use a list that handles this case
|
2017-11-09 05:08:31 +01:00
|
|
|
if (!$id) {
|
2016-11-29 00:31:16 +01:00
|
|
|
if (!isset($this->unsavedRelations[$componentName])) {
|
|
|
|
$this->unsavedRelations[$componentName] =
|
2017-05-17 07:40:13 +02:00
|
|
|
new UnsavedRelationList(static::class, $componentName, $componentClass);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
return $this->unsavedRelations[$componentName];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determine type and nature of foreign relation
|
|
|
|
$joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
|
|
|
|
/** @var HasManyList $result */
|
|
|
|
if ($polymorphic) {
|
2017-05-23 01:36:15 +02:00
|
|
|
$result = PolymorphicHasManyList::create($componentClass, $joinField, static::class);
|
2016-11-29 00:31:16 +01:00
|
|
|
} else {
|
|
|
|
$result = HasManyList::create($componentClass, $joinField);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $result
|
|
|
|
->setDataQueryParam($this->getInheritableQueryParams())
|
2017-11-09 05:08:31 +01:00
|
|
|
->forForeignID($id);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find the foreign class of a relation on this DataObject, regardless of the relation type.
|
|
|
|
*
|
|
|
|
* @param string $relationName Relation name.
|
|
|
|
* @return string Class name, or null if not found.
|
|
|
|
*/
|
|
|
|
public function getRelationClass($relationName)
|
|
|
|
{
|
|
|
|
// Parse many_many
|
|
|
|
$manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
|
|
|
|
if ($manyManyComponent) {
|
2017-05-17 07:40:13 +02:00
|
|
|
return $manyManyComponent['childClass'];
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Go through all relationship configuration fields.
|
2017-02-22 04:14:53 +01:00
|
|
|
$config = $this->config();
|
2016-11-29 00:31:16 +01:00
|
|
|
$candidates = array_merge(
|
2020-04-20 19:58:09 +02:00
|
|
|
($relations = $config->get('has_one')) ? $relations : [],
|
|
|
|
($relations = $config->get('has_many')) ? $relations : [],
|
|
|
|
($relations = $config->get('belongs_to')) ? $relations : []
|
2016-11-29 00:31:16 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
if (isset($candidates[$relationName])) {
|
|
|
|
$remoteClass = $candidates[$relationName];
|
|
|
|
|
|
|
|
// If dot notation is present, extract just the first part that contains the class.
|
2018-01-30 06:28:28 +01:00
|
|
|
if (($fieldPos = strpos($remoteClass, '.')) !== false) {
|
2016-11-29 00:31:16 +01:00
|
|
|
return substr($remoteClass, 0, $fieldPos);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise just return the class
|
|
|
|
return $remoteClass;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a relation name, determine the relation type
|
|
|
|
*
|
|
|
|
* @param string $component Name of component
|
|
|
|
* @return string has_one, has_many, many_many, belongs_many_many or belongs_to
|
|
|
|
*/
|
|
|
|
public function getRelationType($component)
|
|
|
|
{
|
2020-04-20 19:58:09 +02:00
|
|
|
$types = ['has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to'];
|
2017-02-22 04:14:53 +01:00
|
|
|
$config = $this->config();
|
2016-11-29 00:31:16 +01:00
|
|
|
foreach ($types as $type) {
|
2017-02-22 04:14:53 +01:00
|
|
|
$relations = $config->get($type);
|
2016-11-29 00:31:16 +01:00
|
|
|
if ($relations && isset($relations[$component])) {
|
|
|
|
return $type;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a relation declared on a remote class, generate a substitute component for the opposite
|
|
|
|
* side of the relation.
|
|
|
|
*
|
|
|
|
* Notes on behaviour:
|
|
|
|
* - This can still be used on components that are defined on both sides, but do not need to be.
|
|
|
|
* - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
|
2018-03-28 04:42:34 +02:00
|
|
|
* - Polymorphic relationships do not have two natural endpoints (only on one side)
|
|
|
|
* and thus attempting to infer them will return nothing.
|
2016-11-29 00:31:16 +01:00
|
|
|
* - Cannot be used on unsaved objects.
|
|
|
|
*
|
|
|
|
* @param string $remoteClass
|
|
|
|
* @param string $remoteRelation
|
|
|
|
* @return DataList|DataObject The component, either as a list or single object
|
|
|
|
* @throws BadMethodCallException
|
|
|
|
* @throws InvalidArgumentException
|
|
|
|
*/
|
|
|
|
public function inferReciprocalComponent($remoteClass, $remoteRelation)
|
|
|
|
{
|
|
|
|
$remote = DataObject::singleton($remoteClass);
|
|
|
|
$class = $remote->getRelationClass($remoteRelation);
|
|
|
|
$schema = static::getSchema();
|
|
|
|
|
|
|
|
// Validate arguments
|
|
|
|
if (!$this->isInDB()) {
|
|
|
|
throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
|
|
|
|
}
|
|
|
|
if (empty($class)) {
|
|
|
|
throw new InvalidArgumentException(sprintf(
|
|
|
|
"%s invoked with invalid relation %s.%s",
|
|
|
|
__METHOD__,
|
|
|
|
$remoteClass,
|
|
|
|
$remoteRelation
|
|
|
|
));
|
|
|
|
}
|
2018-03-28 04:42:34 +02:00
|
|
|
// If relation is polymorphic, do not infer recriprocal relationship
|
2016-11-29 00:31:16 +01:00
|
|
|
if ($class === self::class) {
|
2018-03-28 04:42:34 +02:00
|
|
|
return null;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
if (!is_a($this, $class, true)) {
|
|
|
|
throw new InvalidArgumentException(sprintf(
|
|
|
|
"Relation %s on %s does not refer to objects of type %s",
|
|
|
|
$remoteRelation,
|
|
|
|
$remoteClass,
|
|
|
|
static::class
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check the relation type to mock
|
|
|
|
$relationType = $remote->getRelationType($remoteRelation);
|
|
|
|
switch ($relationType) {
|
|
|
|
case 'has_one': {
|
|
|
|
// Mock has_many
|
|
|
|
$joinField = "{$remoteRelation}ID";
|
|
|
|
$componentClass = $schema->classForField($remoteClass, $joinField);
|
|
|
|
$result = HasManyList::create($componentClass, $joinField);
|
|
|
|
return $result
|
|
|
|
->setDataQueryParam($this->getInheritableQueryParams())
|
|
|
|
->forForeignID($this->ID);
|
|
|
|
}
|
|
|
|
case 'belongs_to':
|
|
|
|
case 'has_many': {
|
|
|
|
// These relations must have a has_one on the other end, so find it
|
2018-01-30 06:28:28 +01:00
|
|
|
$joinField = $schema->getRemoteJoinField(
|
|
|
|
$remoteClass,
|
|
|
|
$remoteRelation,
|
|
|
|
$relationType,
|
|
|
|
$polymorphic
|
|
|
|
);
|
2018-03-28 04:42:34 +02:00
|
|
|
// If relation is polymorphic, do not infer recriprocal relationship automatically
|
2016-11-29 00:31:16 +01:00
|
|
|
if ($polymorphic) {
|
2018-03-28 04:42:34 +02:00
|
|
|
return null;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
$joinID = $this->getField($joinField);
|
|
|
|
if (empty($joinID)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
// Get object by joined ID
|
|
|
|
return DataObject::get($remoteClass)
|
|
|
|
->filter('ID', $joinID)
|
|
|
|
->setDataQueryParam($this->getInheritableQueryParams())
|
|
|
|
->first();
|
|
|
|
}
|
|
|
|
case 'many_many':
|
|
|
|
case 'belongs_many_many': {
|
|
|
|
// Get components and extra fields from parent
|
2017-05-17 07:40:13 +02:00
|
|
|
$manyMany = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
|
2020-04-20 19:58:09 +02:00
|
|
|
$extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
// Reverse parent and component fields and create an inverse ManyManyList
|
|
|
|
/** @var RelationList $result */
|
|
|
|
$result = Injector::inst()->create(
|
2017-05-17 07:40:13 +02:00
|
|
|
$manyMany['relationClass'],
|
|
|
|
$manyMany['parentClass'], // Substitute parent class for dataClass
|
|
|
|
$manyMany['join'],
|
|
|
|
$manyMany['parentField'], // Reversed parent / child field
|
|
|
|
$manyMany['childField'], // Reversed parent / child field
|
2018-03-21 22:26:25 +01:00
|
|
|
$extraFields,
|
|
|
|
$manyMany['childClass'], // substitute child class for parentClass
|
|
|
|
$remoteClass // In case ManyManyThroughList needs to use PolymorphicHasManyList internally
|
2016-11-29 00:31:16 +01:00
|
|
|
);
|
|
|
|
$this->extend('updateManyManyComponents', $result);
|
|
|
|
|
|
|
|
// If this is called on a singleton, then we return an 'orphaned relation' that can have the
|
|
|
|
// foreignID set elsewhere.
|
|
|
|
return $result
|
|
|
|
->setDataQueryParam($this->getInheritableQueryParams())
|
|
|
|
->forForeignID($this->ID);
|
|
|
|
}
|
|
|
|
default: {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a many-to-many component, as a ManyManyList.
|
|
|
|
* @param string $componentName Name of the many-many component
|
2017-11-09 05:08:31 +01:00
|
|
|
* @param int|array $id Optional ID for parent of this relation, if not the current record
|
2018-01-30 06:28:28 +01:00
|
|
|
* @return ManyManyList|UnsavedRelationList The set of components
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2017-11-09 05:08:31 +01:00
|
|
|
public function getManyManyComponents($componentName, $id = null)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2017-11-09 05:08:31 +01:00
|
|
|
if (!isset($id)) {
|
|
|
|
$id = $this->ID;
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
$schema = static::getSchema();
|
|
|
|
$manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
|
|
|
|
if (!$manyManyComponent) {
|
|
|
|
throw new InvalidArgumentException(sprintf(
|
|
|
|
"DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
|
|
|
|
$componentName,
|
2017-05-17 07:40:13 +02:00
|
|
|
static::class
|
2016-11-29 00:31:16 +01:00
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we haven't been written yet, we can't save these relations, so use a list that handles this case
|
2017-11-09 05:08:31 +01:00
|
|
|
if (!$id) {
|
2016-11-29 00:31:16 +01:00
|
|
|
if (!isset($this->unsavedRelations[$componentName])) {
|
2019-10-18 04:31:39 +02:00
|
|
|
$this->unsavedRelations[$componentName] = new UnsavedRelationList(
|
|
|
|
$manyManyComponent['parentClass'],
|
|
|
|
$componentName,
|
|
|
|
$manyManyComponent['childClass']
|
|
|
|
);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
return $this->unsavedRelations[$componentName];
|
|
|
|
}
|
|
|
|
|
2020-04-20 19:58:09 +02:00
|
|
|
$extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: [];
|
2016-11-29 00:31:16 +01:00
|
|
|
/** @var RelationList $result */
|
|
|
|
$result = Injector::inst()->create(
|
2017-05-17 07:40:13 +02:00
|
|
|
$manyManyComponent['relationClass'],
|
|
|
|
$manyManyComponent['childClass'],
|
|
|
|
$manyManyComponent['join'],
|
|
|
|
$manyManyComponent['childField'],
|
|
|
|
$manyManyComponent['parentField'],
|
2018-03-21 22:26:25 +01:00
|
|
|
$extraFields,
|
|
|
|
$manyManyComponent['parentClass'],
|
|
|
|
static::class // In case ManyManyThroughList needs to use PolymorphicHasManyList internally
|
2016-11-29 00:31:16 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
// Store component data in query meta-data
|
|
|
|
$result = $result->alterDataQuery(function ($query) use ($extraFields) {
|
|
|
|
/** @var DataQuery $query */
|
|
|
|
$query->setQueryParam('Component.ExtraFields', $extraFields);
|
|
|
|
});
|
|
|
|
|
2018-11-01 01:42:27 +01:00
|
|
|
// If we have a default sort set for our "join" then we should overwrite any default already set.
|
|
|
|
$joinSort = Config::inst()->get($manyManyComponent['join'], 'default_sort');
|
|
|
|
if (!empty($joinSort)) {
|
|
|
|
$result = $result->sort($joinSort);
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
$this->extend('updateManyManyComponents', $result);
|
|
|
|
|
|
|
|
// If this is called on a singleton, then we return an 'orphaned relation' that can have the
|
|
|
|
// foreignID set elsewhere.
|
|
|
|
return $result
|
|
|
|
->setDataQueryParam($this->getInheritableQueryParams())
|
2017-11-09 05:08:31 +01:00
|
|
|
->forForeignID($id);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the class of a one-to-one component. If $component is null, return all of the one-to-one components and
|
|
|
|
* their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
|
|
|
|
*
|
|
|
|
* @return string|array The class of the one-to-one component, or an array of all one-to-one components and
|
|
|
|
* their classes.
|
|
|
|
*/
|
|
|
|
public function hasOne()
|
|
|
|
{
|
2017-02-22 04:14:53 +01:00
|
|
|
return (array)$this->config()->get('has_one');
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
|
|
|
|
* their class name will be returned.
|
|
|
|
*
|
|
|
|
* @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
|
|
|
|
* the field data stripped off. It defaults to TRUE.
|
|
|
|
* @return string|array
|
|
|
|
*/
|
|
|
|
public function belongsTo($classOnly = true)
|
|
|
|
{
|
2017-02-22 04:14:53 +01:00
|
|
|
$belongsTo = (array)$this->config()->get('belongs_to');
|
2016-11-29 00:31:16 +01:00
|
|
|
if ($belongsTo && $classOnly) {
|
|
|
|
return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
|
|
|
|
} else {
|
2020-04-20 19:58:09 +02:00
|
|
|
return $belongsTo ? $belongsTo : [];
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
|
|
|
|
* relationships and their classes will be returned.
|
|
|
|
*
|
|
|
|
* @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
|
|
|
|
* the field data stripped off. It defaults to TRUE.
|
|
|
|
* @return string|array|false
|
|
|
|
*/
|
|
|
|
public function hasMany($classOnly = true)
|
|
|
|
{
|
2017-02-22 04:14:53 +01:00
|
|
|
$hasMany = (array)$this->config()->get('has_many');
|
2016-11-29 00:31:16 +01:00
|
|
|
if ($hasMany && $classOnly) {
|
|
|
|
return preg_replace('/(.+)?\..+/', '$1', $hasMany);
|
|
|
|
} else {
|
2020-04-20 19:58:09 +02:00
|
|
|
return $hasMany ? $hasMany : [];
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the many-to-many extra fields specification.
|
|
|
|
*
|
|
|
|
* If you don't specify a component name, it returns all
|
|
|
|
* extra fields for all components available.
|
|
|
|
*
|
|
|
|
* @return array|null
|
|
|
|
*/
|
|
|
|
public function manyManyExtraFields()
|
|
|
|
{
|
2017-02-22 04:14:53 +01:00
|
|
|
return $this->config()->get('many_many_extraFields');
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return information about a many-to-many component.
|
|
|
|
* The return value is an array of (parentclass, childclass). If $component is null, then all many-many
|
|
|
|
* components are returned.
|
|
|
|
*
|
|
|
|
* @see DataObjectSchema::manyManyComponent()
|
|
|
|
* @return array|null An array of (parentclass, childclass), or an array of all many-many components
|
|
|
|
*/
|
|
|
|
public function manyMany()
|
|
|
|
{
|
2017-02-22 04:14:53 +01:00
|
|
|
$config = $this->config();
|
|
|
|
$manyManys = (array)$config->get('many_many');
|
|
|
|
$belongsManyManys = (array)$config->get('belongs_many_many');
|
2016-11-29 00:31:16 +01:00
|
|
|
$items = array_merge($manyManys, $belongsManyManys);
|
|
|
|
return $items;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This returns an array (if it exists) describing the database extensions that are required, or false if none
|
|
|
|
*
|
|
|
|
* This is experimental, and is currently only a Postgres-specific enhancement.
|
|
|
|
*
|
|
|
|
* @param string $class
|
|
|
|
* @return array|false
|
|
|
|
*/
|
|
|
|
public function database_extensions($class)
|
|
|
|
{
|
|
|
|
$extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
|
|
|
|
if ($extensions) {
|
|
|
|
return $extensions;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates a SearchContext to be used for building and processing
|
|
|
|
* a generic search form for properties on this object.
|
|
|
|
*
|
|
|
|
* @return SearchContext
|
|
|
|
*/
|
|
|
|
public function getDefaultSearchContext()
|
|
|
|
{
|
2019-08-01 04:39:58 +02:00
|
|
|
return SearchContext::create(
|
2017-05-17 07:40:13 +02:00
|
|
|
static::class,
|
2016-11-29 00:31:16 +01:00
|
|
|
$this->scaffoldSearchFields(),
|
|
|
|
$this->defaultSearchFilters()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine which properties on the DataObject are
|
|
|
|
* searchable, and map them to their default {@link FormField}
|
|
|
|
* representations. Used for scaffolding a searchform for {@link ModelAdmin}.
|
|
|
|
*
|
|
|
|
* Some additional logic is included for switching field labels, based on
|
|
|
|
* how generic or specific the field type is.
|
|
|
|
*
|
|
|
|
* Used by {@link SearchContext}.
|
|
|
|
*
|
|
|
|
* @param array $_params
|
|
|
|
* 'fieldClasses': Associative array of field names as keys and FormField classes as values
|
|
|
|
* 'restrictFields': Numeric array of a field name whitelist
|
|
|
|
* @return FieldList
|
|
|
|
*/
|
|
|
|
public function scaffoldSearchFields($_params = null)
|
|
|
|
{
|
|
|
|
$params = array_merge(
|
2020-04-20 19:58:09 +02:00
|
|
|
[
|
2016-11-29 00:31:16 +01:00
|
|
|
'fieldClasses' => false,
|
|
|
|
'restrictFields' => false
|
2020-04-20 19:58:09 +02:00
|
|
|
],
|
2016-11-29 00:31:16 +01:00
|
|
|
(array)$_params
|
|
|
|
);
|
|
|
|
$fields = new FieldList();
|
|
|
|
foreach ($this->searchableFields() as $fieldName => $spec) {
|
|
|
|
if ($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If a custom fieldclass is provided as a string, use it
|
|
|
|
$field = null;
|
|
|
|
if ($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
|
|
|
|
$fieldClass = $params['fieldClasses'][$fieldName];
|
|
|
|
$field = new $fieldClass($fieldName);
|
|
|
|
// If we explicitly set a field, then construct that
|
|
|
|
} elseif (isset($spec['field'])) {
|
|
|
|
// If it's a string, use it as a class name and construct
|
|
|
|
if (is_string($spec['field'])) {
|
|
|
|
$fieldClass = $spec['field'];
|
|
|
|
$field = new $fieldClass($fieldName);
|
|
|
|
|
|
|
|
// If it's a FormField object, then just use that object directly.
|
|
|
|
} elseif ($spec['field'] instanceof FormField) {
|
|
|
|
$field = $spec['field'];
|
|
|
|
|
|
|
|
// Otherwise we have a bug
|
|
|
|
} else {
|
|
|
|
user_error("Bad value for searchable_fields, 'field' value: "
|
|
|
|
. var_export($spec['field'], true), E_USER_WARNING);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, use the database field's scaffolder
|
2017-11-09 05:08:31 +01:00
|
|
|
} elseif ($object = $this->relObject($fieldName)) {
|
2019-02-11 21:39:27 +01:00
|
|
|
if (is_object($object) && $object->hasMethod('scaffoldSearchField')) {
|
|
|
|
$field = $object->scaffoldSearchField();
|
|
|
|
} else {
|
|
|
|
throw new Exception(sprintf(
|
|
|
|
"SearchField '%s' on '%s' does not return a valid DBField instance.",
|
|
|
|
$fieldName,
|
|
|
|
get_class($this)
|
|
|
|
));
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Allow fields to opt out of search
|
|
|
|
if (!$field) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (strstr($fieldName, '.')) {
|
|
|
|
$field->setName(str_replace('.', '__', $fieldName));
|
|
|
|
}
|
|
|
|
$field->setTitle($spec['title']);
|
|
|
|
|
|
|
|
$fields->push($field);
|
|
|
|
}
|
|
|
|
return $fields;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Scaffold a simple edit form for all properties on this dataobject,
|
|
|
|
* based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
|
|
|
|
* Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
|
|
|
|
*
|
|
|
|
* @uses FormScaffolder
|
|
|
|
*
|
|
|
|
* @param array $_params Associative array passing through properties to {@link FormScaffolder}.
|
|
|
|
* @return FieldList
|
|
|
|
*/
|
|
|
|
public function scaffoldFormFields($_params = null)
|
|
|
|
{
|
|
|
|
$params = array_merge(
|
2020-04-20 19:58:09 +02:00
|
|
|
[
|
2016-11-29 00:31:16 +01:00
|
|
|
'tabbed' => false,
|
|
|
|
'includeRelations' => false,
|
|
|
|
'restrictFields' => false,
|
|
|
|
'fieldClasses' => false,
|
|
|
|
'ajaxSafe' => false
|
2020-04-20 19:58:09 +02:00
|
|
|
],
|
2016-11-29 00:31:16 +01:00
|
|
|
(array)$_params
|
|
|
|
);
|
|
|
|
|
2017-05-17 07:40:13 +02:00
|
|
|
$fs = FormScaffolder::create($this);
|
2016-11-29 00:31:16 +01:00
|
|
|
$fs->tabbed = $params['tabbed'];
|
|
|
|
$fs->includeRelations = $params['includeRelations'];
|
|
|
|
$fs->restrictFields = $params['restrictFields'];
|
|
|
|
$fs->fieldClasses = $params['fieldClasses'];
|
|
|
|
$fs->ajaxSafe = $params['ajaxSafe'];
|
|
|
|
|
2019-06-28 06:11:16 +02:00
|
|
|
$this->extend('updateFormScaffolder', $fs, $this);
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
return $fs->getFieldList();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
|
|
|
|
* being called on extensions
|
|
|
|
*
|
|
|
|
* @param callable $callback The callback to execute
|
|
|
|
*/
|
|
|
|
protected function beforeUpdateCMSFields($callback)
|
|
|
|
{
|
|
|
|
$this->beforeExtending('updateCMSFields', $callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Centerpiece of every data administration interface in Silverstripe,
|
|
|
|
* which returns a {@link FieldList} suitable for a {@link Form} object.
|
|
|
|
* If not overloaded, we're using {@link scaffoldFormFields()} to automatically
|
|
|
|
* generate this set. To customize, overload this method in a subclass
|
|
|
|
* or extended onto it by using {@link DataExtension->updateCMSFields()}.
|
|
|
|
*
|
|
|
|
* <code>
|
|
|
|
* class MyCustomClass extends DataObject {
|
|
|
|
* static $db = array('CustomProperty'=>'Boolean');
|
|
|
|
*
|
|
|
|
* function getCMSFields() {
|
|
|
|
* $fields = parent::getCMSFields();
|
|
|
|
* $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
|
|
|
|
* return $fields;
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
* </code>
|
|
|
|
*
|
|
|
|
* @see Good example of complex FormField building: SiteTree::getCMSFields()
|
|
|
|
*
|
|
|
|
* @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
|
|
|
|
*/
|
|
|
|
public function getCMSFields()
|
|
|
|
{
|
2020-04-20 19:58:09 +02:00
|
|
|
$tabbedFields = $this->scaffoldFormFields([
|
2016-11-29 00:31:16 +01:00
|
|
|
// Don't allow has_many/many_many relationship editing before the record is first saved
|
|
|
|
'includeRelations' => ($this->ID > 0),
|
|
|
|
'tabbed' => true,
|
|
|
|
'ajaxSafe' => true
|
2020-04-20 19:58:09 +02:00
|
|
|
]);
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
$this->extend('updateCMSFields', $tabbedFields);
|
|
|
|
|
|
|
|
return $tabbedFields;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* need to be overload by solid dataobject, so that the customised actions of that dataobject,
|
|
|
|
* including that dataobject's extensions customised actions could be added to the EditForm.
|
|
|
|
*
|
|
|
|
* @return FieldList an Empty FieldList(); need to be overload by solid subclass
|
|
|
|
*/
|
|
|
|
public function getCMSActions()
|
|
|
|
{
|
|
|
|
$actions = new FieldList();
|
|
|
|
$this->extend('updateCMSActions', $actions);
|
|
|
|
return $actions;
|
|
|
|
}
|
|
|
|
|
2020-02-13 20:21:22 +01:00
|
|
|
/**
|
2020-05-28 02:23:35 +02:00
|
|
|
* When extending this class and overriding this method, you will need to instantiate the CompositeValidator by
|
|
|
|
* calling parent::getCMSCompositeValidator(). This will ensure that the appropriate extension point is also
|
|
|
|
* invoked.
|
|
|
|
*
|
|
|
|
* You can also update the CompositeValidator by creating an Extension and implementing the
|
|
|
|
* updateCMSCompositeValidator(CompositeValidator $compositeValidator) method.
|
|
|
|
*
|
|
|
|
* @see CompositeValidator for examples of implementation
|
2020-05-27 23:35:07 +02:00
|
|
|
* @return CompositeValidator
|
2020-02-13 20:21:22 +01:00
|
|
|
*/
|
2020-05-28 02:23:35 +02:00
|
|
|
public function getCMSCompositeValidator(): CompositeValidator
|
2020-02-13 00:51:56 +01:00
|
|
|
{
|
2020-05-27 23:35:07 +02:00
|
|
|
$compositeValidator = new CompositeValidator();
|
2020-02-13 00:51:56 +01:00
|
|
|
|
|
|
|
// Support for the old method during the deprecation period
|
|
|
|
if ($this->hasMethod('getCMSValidator')) {
|
2020-02-18 23:56:10 +01:00
|
|
|
Deprecation::notice(
|
|
|
|
'4.6',
|
2020-05-28 02:23:35 +02:00
|
|
|
'getCMSValidator() is removed in 5.0 in favour of getCMSCompositeValidator()'
|
2020-02-18 23:56:10 +01:00
|
|
|
);
|
|
|
|
|
2020-05-27 23:35:07 +02:00
|
|
|
$compositeValidator->addValidator($this->getCMSValidator());
|
2020-02-13 00:51:56 +01:00
|
|
|
}
|
|
|
|
|
2020-05-27 23:35:07 +02:00
|
|
|
// Extend validator - forward support, will be supported beyond 5.0.0
|
2020-05-28 02:23:35 +02:00
|
|
|
$this->invokeWithExtensions('updateCMSCompositeValidator', $compositeValidator);
|
2020-02-13 00:51:56 +01:00
|
|
|
|
2020-05-27 23:35:07 +02:00
|
|
|
return $compositeValidator;
|
2020-02-13 00:51:56 +01:00
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Used for simple frontend forms without relation editing
|
|
|
|
* or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
|
|
|
|
* by default. To customize, either overload this method in your
|
|
|
|
* subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
|
|
|
|
*
|
|
|
|
* @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
|
|
|
|
*
|
|
|
|
* @param array $params See {@link scaffoldFormFields()}
|
|
|
|
* @return FieldList Always returns a simple field collection without TabSet.
|
|
|
|
*/
|
|
|
|
public function getFrontEndFields($params = null)
|
|
|
|
{
|
|
|
|
$untabbedFields = $this->scaffoldFormFields($params);
|
|
|
|
$this->extend('updateFrontEndFields', $untabbedFields);
|
|
|
|
|
|
|
|
return $untabbedFields;
|
|
|
|
}
|
|
|
|
|
2017-12-01 16:03:39 +01:00
|
|
|
public function getViewerTemplates($suffix = '')
|
|
|
|
{
|
|
|
|
return SSViewer::get_templates_by_class(static::class, $suffix, $this->baseClass());
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* Gets the value of a field.
|
|
|
|
* Called by {@link __get()} and any getFieldName() methods you might create.
|
|
|
|
*
|
|
|
|
* @param string $field The name of the field
|
|
|
|
* @return mixed The field value
|
|
|
|
*/
|
|
|
|
public function getField($field)
|
|
|
|
{
|
2018-02-05 04:07:35 +01:00
|
|
|
// If we already have a value in $this->record, then we should just return that
|
|
|
|
if (isset($this->record[$field])) {
|
2016-11-29 00:31:16 +01:00
|
|
|
return $this->record[$field];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do we have a field that needs to be lazy loaded?
|
2018-01-16 19:39:30 +01:00
|
|
|
if (isset($this->record[$field . '_Lazy'])) {
|
|
|
|
$tableClass = $this->record[$field . '_Lazy'];
|
2016-11-29 00:31:16 +01:00
|
|
|
$this->loadLazyFields($tableClass);
|
|
|
|
}
|
2018-01-30 06:28:28 +01:00
|
|
|
$schema = static::getSchema();
|
|
|
|
|
|
|
|
// Support unary relations as fields
|
|
|
|
if ($schema->unaryComponent(static::class, $field)) {
|
|
|
|
return $this->getComponent($field);
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
// In case of complex fields, return the DBField object
|
2018-01-30 06:28:28 +01:00
|
|
|
if ($schema->compositeField(static::class, $field)) {
|
2016-11-29 00:31:16 +01:00
|
|
|
$this->record[$field] = $this->dbObject($field);
|
|
|
|
}
|
|
|
|
|
|
|
|
return isset($this->record[$field]) ? $this->record[$field] : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Loads all the stub fields that an initial lazy load didn't load fully.
|
|
|
|
*
|
|
|
|
* @param string $class Class to load the values from. Others are joined as required.
|
|
|
|
* Not specifying a tableClass will load all lazy fields from all tables.
|
|
|
|
* @return bool Flag if lazy loading succeeded
|
|
|
|
*/
|
|
|
|
protected function loadLazyFields($class = null)
|
|
|
|
{
|
|
|
|
if (!$this->isInDB() || !is_numeric($this->ID)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$class) {
|
2020-04-20 19:58:09 +02:00
|
|
|
$loaded = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
foreach ($this->record as $key => $value) {
|
|
|
|
if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
|
|
|
|
$this->loadLazyFields($value);
|
|
|
|
$loaded[$value] = $value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$dataQuery = new DataQuery($class);
|
|
|
|
|
|
|
|
// Reset query parameter context to that of this DataObject
|
|
|
|
if ($params = $this->getSourceQueryParams()) {
|
|
|
|
foreach ($params as $key => $value) {
|
|
|
|
$dataQuery->setQueryParam($key, $value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Limit query to the current record, unless it has the Versioned extension,
|
|
|
|
// in which case it requires special handling through augmentLoadLazyFields()
|
|
|
|
$schema = static::getSchema();
|
|
|
|
$baseIDColumn = $schema->sqlColumnForField($this, 'ID');
|
|
|
|
$dataQuery->where([
|
|
|
|
$baseIDColumn => $this->record['ID']
|
|
|
|
])->limit(1);
|
|
|
|
|
2020-04-20 19:58:09 +02:00
|
|
|
$columns = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
// Add SQL for fields, both simple & multi-value
|
|
|
|
// TODO: This is copy & pasted from buildSQL(), it could be moved into a method
|
|
|
|
$databaseFields = $schema->databaseFields($class, false);
|
|
|
|
foreach ($databaseFields as $k => $v) {
|
|
|
|
if (!isset($this->record[$k]) || $this->record[$k] === null) {
|
|
|
|
$columns[] = $k;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($columns) {
|
|
|
|
$query = $dataQuery->query();
|
|
|
|
$this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
|
|
|
|
$this->extend('augmentSQL', $query, $dataQuery);
|
|
|
|
|
|
|
|
$dataQuery->setQueriedColumns($columns);
|
|
|
|
$newData = $dataQuery->execute()->record();
|
|
|
|
|
|
|
|
// Load the data into record
|
|
|
|
if ($newData) {
|
|
|
|
foreach ($newData as $k => $v) {
|
|
|
|
if (in_array($k, $columns)) {
|
|
|
|
$this->record[$k] = $v;
|
|
|
|
$this->original[$k] = $v;
|
|
|
|
unset($this->record[$k . '_Lazy']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// No data means that the query returned nothing; assign 'null' to all the requested fields
|
|
|
|
} else {
|
|
|
|
foreach ($columns as $k) {
|
|
|
|
$this->record[$k] = null;
|
|
|
|
$this->original[$k] = null;
|
|
|
|
unset($this->record[$k . '_Lazy']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-10-05 00:09:00 +02:00
|
|
|
* Return the fields that have changed since the last write.
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
|
|
|
* The change level affects what the functions defines as "changed":
|
|
|
|
* - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
|
|
|
|
* - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
|
|
|
|
* for example a change from 0 to null would not be included.
|
|
|
|
*
|
|
|
|
* Example return:
|
|
|
|
* <code>
|
|
|
|
* array(
|
|
|
|
* 'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
|
|
|
|
* )
|
|
|
|
* </code>
|
|
|
|
*
|
|
|
|
* @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
|
|
|
|
* to return all database fields, or an array for an explicit filter. false returns all fields.
|
|
|
|
* @param int $changeLevel The strictness of what is defined as change. Defaults to strict
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT)
|
|
|
|
{
|
2020-04-20 19:58:09 +02:00
|
|
|
$changedFields = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
// Update the changed array with references to changed obj-fields
|
|
|
|
foreach ($this->record as $k => $v) {
|
|
|
|
// Prevents DBComposite infinite looping on isChanged
|
|
|
|
if (is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
|
|
|
|
$this->changed[$k] = self::CHANGE_VALUE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-08 03:55:02 +02:00
|
|
|
// If change was forced, then derive change data from $this->record
|
|
|
|
if ($this->changeForced && $changeLevel <= self::CHANGE_STRICT) {
|
|
|
|
$changed = array_combine(
|
|
|
|
array_keys($this->record),
|
|
|
|
array_fill(0, count($this->record), self::CHANGE_STRICT)
|
|
|
|
);
|
|
|
|
// @todo Find better way to allow versioned to write a new version after forceChange
|
|
|
|
unset($changed['Version']);
|
|
|
|
} else {
|
|
|
|
$changed = $this->changed;
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
if (is_array($databaseFieldsOnly)) {
|
2018-10-08 03:55:02 +02:00
|
|
|
$fields = array_intersect_key($changed, array_flip($databaseFieldsOnly));
|
2016-11-29 00:31:16 +01:00
|
|
|
} elseif ($databaseFieldsOnly) {
|
|
|
|
$fieldsSpecs = static::getSchema()->fieldSpecs(static::class);
|
2018-10-08 03:55:02 +02:00
|
|
|
$fields = array_intersect_key($changed, $fieldsSpecs);
|
2016-11-29 00:31:16 +01:00
|
|
|
} else {
|
2018-10-08 03:55:02 +02:00
|
|
|
$fields = $changed;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Filter the list to those of a certain change level
|
|
|
|
if ($changeLevel > self::CHANGE_STRICT) {
|
|
|
|
if ($fields) {
|
|
|
|
foreach ($fields as $name => $level) {
|
|
|
|
if ($level < $changeLevel) {
|
|
|
|
unset($fields[$name]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($fields) {
|
|
|
|
foreach ($fields as $name => $level) {
|
2020-04-20 19:58:09 +02:00
|
|
|
$changedFields[$name] = [
|
2018-01-30 06:28:28 +01:00
|
|
|
'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
|
|
|
|
'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
|
|
|
|
'level' => $level
|
2020-04-20 19:58:09 +02:00
|
|
|
];
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $changedFields;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Uses {@link getChangedFields()} to determine if fields have been changed
|
|
|
|
* since loading them from the database.
|
|
|
|
*
|
|
|
|
* @param string $fieldName Name of the database field to check, will check for any if not given
|
|
|
|
* @param int $changeLevel See {@link getChangedFields()}
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT)
|
|
|
|
{
|
2020-04-20 19:58:09 +02:00
|
|
|
$fields = $fieldName ? [$fieldName] : true;
|
2016-11-29 00:31:16 +01:00
|
|
|
$changed = $this->getChangedFields($fields, $changeLevel);
|
|
|
|
if (!isset($fieldName)) {
|
|
|
|
return !empty($changed);
|
|
|
|
} else {
|
|
|
|
return array_key_exists($fieldName, $changed);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the value of the field
|
|
|
|
* Called by {@link __set()} and any setFieldName() methods you might create.
|
|
|
|
*
|
|
|
|
* @param string $fieldName Name of the field
|
|
|
|
* @param mixed $val New field value
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setField($fieldName, $val)
|
|
|
|
{
|
|
|
|
$this->objCacheClear();
|
|
|
|
//if it's a has_one component, destroy the cache
|
|
|
|
if (substr($fieldName, -2) == 'ID') {
|
|
|
|
unset($this->components[substr($fieldName, 0, -2)]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we've just lazy-loaded the column, then we need to populate the $original array
|
2018-01-16 19:39:30 +01:00
|
|
|
if (isset($this->record[$fieldName . '_Lazy'])) {
|
|
|
|
$tableClass = $this->record[$fieldName . '_Lazy'];
|
2016-11-29 00:31:16 +01:00
|
|
|
$this->loadLazyFields($tableClass);
|
|
|
|
}
|
|
|
|
|
2018-01-30 06:28:28 +01:00
|
|
|
// Support component assignent via field setter
|
|
|
|
$schema = static::getSchema();
|
|
|
|
if ($schema->unaryComponent(static::class, $fieldName)) {
|
2018-02-05 04:07:35 +01:00
|
|
|
unset($this->components[$fieldName]);
|
2018-01-30 06:28:28 +01:00
|
|
|
// Assign component directly
|
|
|
|
if (is_null($val) || $val instanceof DataObject) {
|
|
|
|
return $this->setComponent($fieldName, $val);
|
|
|
|
}
|
|
|
|
// Assign by ID instead of object
|
2018-02-05 04:07:35 +01:00
|
|
|
if (is_numeric($val)) {
|
|
|
|
$fieldName .= 'ID';
|
|
|
|
}
|
2018-01-30 06:28:28 +01:00
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
// Situation 1: Passing an DBField
|
|
|
|
if ($val instanceof DBField) {
|
|
|
|
$val->setName($fieldName);
|
|
|
|
$val->saveInto($this);
|
|
|
|
|
|
|
|
// Situation 1a: Composite fields should remain bound in case they are
|
|
|
|
// later referenced to update the parent dataobject
|
|
|
|
if ($val instanceof DBComposite) {
|
|
|
|
$val->bindTo($this);
|
|
|
|
$this->record[$fieldName] = $val;
|
|
|
|
}
|
|
|
|
// Situation 2: Passing a literal or non-DBField object
|
|
|
|
} else {
|
|
|
|
// If this is a proper database field, we shouldn't be getting non-DBField objects
|
2018-01-30 06:28:28 +01:00
|
|
|
if (is_object($val) && $schema->fieldSpec(static::class, $fieldName)) {
|
2016-11-29 00:31:16 +01:00
|
|
|
throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
|
|
|
|
}
|
|
|
|
|
2018-12-18 04:46:56 +01:00
|
|
|
if (!empty($val) && !is_scalar($val)) {
|
|
|
|
$dbField = $this->dbObject($fieldName);
|
|
|
|
if ($dbField && $dbField->scalarValueOnly()) {
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
sprintf(
|
|
|
|
'DataObject::setField: %s only accepts scalars',
|
|
|
|
$fieldName
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
// if a field is not existing or has strictly changed
|
2019-01-31 13:30:12 +01:00
|
|
|
if (!array_key_exists($fieldName, $this->original) || $this->original[$fieldName] !== $val) {
|
2016-11-29 00:31:16 +01:00
|
|
|
// TODO Add check for php-level defaults which are not set in the db
|
|
|
|
// TODO Add check for hidden input-fields (readonly) which are not set in the db
|
|
|
|
// At the very least, the type has changed
|
|
|
|
$this->changed[$fieldName] = self::CHANGE_STRICT;
|
|
|
|
|
2019-01-31 13:30:12 +01:00
|
|
|
if ((!array_key_exists($fieldName, $this->original) && $val)
|
|
|
|
|| (array_key_exists($fieldName, $this->original) && $this->original[$fieldName] != $val)
|
2016-11-29 00:31:16 +01:00
|
|
|
) {
|
|
|
|
// Value has changed as well, not just the type
|
|
|
|
$this->changed[$fieldName] = self::CHANGE_VALUE;
|
|
|
|
}
|
2018-10-05 00:09:00 +02:00
|
|
|
// Value has been restored to its original, remove any record of the change
|
2018-10-08 03:55:02 +02:00
|
|
|
} elseif (isset($this->changed[$fieldName])) {
|
2018-10-05 00:09:00 +02:00
|
|
|
unset($this->changed[$fieldName]);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
2018-10-05 00:09:00 +02:00
|
|
|
|
|
|
|
// Value is saved regardless, since the change detection relates to the last write
|
|
|
|
$this->record[$fieldName] = $val;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the value of the field, using a casting object.
|
|
|
|
* This is useful when you aren't sure that a date is in SQL format, for example.
|
|
|
|
* setCastedField() can also be used, by forms, to set related data. For example, uploaded images
|
|
|
|
* can be saved into the Image table.
|
|
|
|
*
|
|
|
|
* @param string $fieldName Name of the field
|
|
|
|
* @param mixed $value New field value
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setCastedField($fieldName, $value)
|
|
|
|
{
|
|
|
|
if (!$fieldName) {
|
2020-09-25 02:09:37 +02:00
|
|
|
throw new InvalidArgumentException("DataObject::setCastedField: Called without a fieldName");
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
$fieldObj = $this->dbObject($fieldName);
|
|
|
|
if ($fieldObj) {
|
|
|
|
$fieldObj->setValue($value);
|
|
|
|
$fieldObj->saveInto($this);
|
|
|
|
} else {
|
|
|
|
$this->$fieldName = $value;
|
|
|
|
}
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* {@inheritdoc}
|
|
|
|
*/
|
|
|
|
public function castingHelper($field)
|
|
|
|
{
|
|
|
|
$fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
|
|
|
|
if ($fieldSpec) {
|
|
|
|
return $fieldSpec;
|
|
|
|
}
|
|
|
|
|
|
|
|
// many_many_extraFields aren't presented by db(), so we check if the source query params
|
|
|
|
// provide us with meta-data for a many_many relation we can inspect for extra fields.
|
|
|
|
$queryParams = $this->getSourceQueryParams();
|
|
|
|
if (!empty($queryParams['Component.ExtraFields'])) {
|
|
|
|
$extraFields = $queryParams['Component.ExtraFields'];
|
|
|
|
|
|
|
|
if (isset($extraFields[$field])) {
|
|
|
|
return $extraFields[$field];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return parent::castingHelper($field);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the given field exists in a database column on any of
|
|
|
|
* the objects tables and optionally look up a dynamic getter with
|
|
|
|
* get<fieldName>().
|
|
|
|
*
|
|
|
|
* @param string $field Name of the field
|
|
|
|
* @return boolean True if the given field exists
|
|
|
|
*/
|
|
|
|
public function hasField($field)
|
|
|
|
{
|
|
|
|
$schema = static::getSchema();
|
|
|
|
return (
|
|
|
|
array_key_exists($field, $this->record)
|
2018-01-30 06:28:28 +01:00
|
|
|
|| array_key_exists($field, $this->components)
|
2016-11-29 00:31:16 +01:00
|
|
|
|| $schema->fieldSpec(static::class, $field)
|
2018-01-30 06:28:28 +01:00
|
|
|
|| $schema->unaryComponent(static::class, $field)
|
2016-11-29 00:31:16 +01:00
|
|
|
|| $this->hasMethod("get{$field}")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the given field exists as a database column
|
|
|
|
*
|
|
|
|
* @param string $field Name of the field
|
|
|
|
*
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
public function hasDatabaseField($field)
|
|
|
|
{
|
|
|
|
$spec = static::getSchema()->fieldSpec(static::class, $field, DataObjectSchema::DB_ONLY);
|
|
|
|
return !empty($spec);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the member is allowed to do the given action.
|
|
|
|
* See {@link extendedCan()} for a more versatile tri-state permission control.
|
|
|
|
*
|
|
|
|
* @param string $perm The permission to be checked, such as 'View'.
|
|
|
|
* @param Member $member The member whose permissions need checking. Defaults to the currently logged
|
|
|
|
* in user.
|
|
|
|
* @param array $context Additional $context to pass to extendedCan()
|
|
|
|
*
|
|
|
|
* @return boolean True if the the member is allowed to do the given action
|
|
|
|
*/
|
2020-04-20 19:58:09 +02:00
|
|
|
public function can($perm, $member = null, $context = [])
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2017-05-11 11:07:27 +02:00
|
|
|
if (!$member) {
|
2017-05-20 06:32:25 +02:00
|
|
|
$member = Security::getCurrentUser();
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
2017-02-22 04:14:53 +01:00
|
|
|
|
|
|
|
if ($member && Permission::checkMember($member, "ADMIN")) {
|
2016-11-29 00:31:16 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2017-02-22 04:14:53 +01:00
|
|
|
if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
|
|
|
|
$method = 'can' . ucfirst($perm);
|
|
|
|
return $this->$method($member);
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-02-22 04:14:53 +01:00
|
|
|
$results = $this->extendedCan('can', $member);
|
|
|
|
if (isset($results)) {
|
|
|
|
return $results;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
2017-02-22 04:14:53 +01:00
|
|
|
|
|
|
|
return ($member && Permission::checkMember($member, $perm));
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process tri-state responses from permission-alterting extensions. The extensions are
|
|
|
|
* expected to return one of three values:
|
|
|
|
*
|
|
|
|
* - false: Disallow this permission, regardless of what other extensions say
|
|
|
|
* - true: Allow this permission, as long as no other extensions return false
|
|
|
|
* - NULL: Don't affect the outcome
|
|
|
|
*
|
|
|
|
* This method itself returns a tri-state value, and is designed to be used like this:
|
|
|
|
*
|
|
|
|
* <code>
|
|
|
|
* $extended = $this->extendedCan('canDoSomething', $member);
|
|
|
|
* if($extended !== null) return $extended;
|
|
|
|
* else return $normalValue;
|
|
|
|
* </code>
|
|
|
|
*
|
|
|
|
* @param string $methodName Method on the same object, e.g. {@link canEdit()}
|
|
|
|
* @param Member|int $member
|
|
|
|
* @param array $context Optional context
|
|
|
|
* @return boolean|null
|
|
|
|
*/
|
2020-04-20 19:58:09 +02:00
|
|
|
public function extendedCan($methodName, $member, $context = [])
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
|
|
|
$results = $this->extend($methodName, $member, $context);
|
|
|
|
if ($results && is_array($results)) {
|
|
|
|
// Remove NULLs
|
|
|
|
$results = array_filter($results, function ($v) {
|
|
|
|
return !is_null($v);
|
|
|
|
});
|
|
|
|
// If there are any non-NULL responses, then return the lowest one of them.
|
|
|
|
// If any explicitly deny the permission, then we don't get access
|
|
|
|
if ($results) {
|
|
|
|
return min($results);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param Member $member
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
public function canView($member = null)
|
|
|
|
{
|
|
|
|
$extended = $this->extendedCan(__FUNCTION__, $member);
|
|
|
|
if ($extended !== null) {
|
|
|
|
return $extended;
|
|
|
|
}
|
|
|
|
return Permission::check('ADMIN', 'any', $member);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param Member $member
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
public function canEdit($member = null)
|
|
|
|
{
|
|
|
|
$extended = $this->extendedCan(__FUNCTION__, $member);
|
|
|
|
if ($extended !== null) {
|
|
|
|
return $extended;
|
|
|
|
}
|
|
|
|
return Permission::check('ADMIN', 'any', $member);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param Member $member
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
public function canDelete($member = null)
|
|
|
|
{
|
|
|
|
$extended = $this->extendedCan(__FUNCTION__, $member);
|
|
|
|
if ($extended !== null) {
|
|
|
|
return $extended;
|
|
|
|
}
|
|
|
|
return Permission::check('ADMIN', 'any', $member);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param Member $member
|
|
|
|
* @param array $context Additional context-specific data which might
|
|
|
|
* affect whether (or where) this object could be created.
|
|
|
|
* @return boolean
|
|
|
|
*/
|
2020-04-20 19:58:09 +02:00
|
|
|
public function canCreate($member = null, $context = [])
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
|
|
|
$extended = $this->extendedCan(__FUNCTION__, $member, $context);
|
|
|
|
if ($extended !== null) {
|
|
|
|
return $extended;
|
|
|
|
}
|
|
|
|
return Permission::check('ADMIN', 'any', $member);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Debugging used by Debug::show()
|
|
|
|
*
|
|
|
|
* @return string HTML data representing this object
|
|
|
|
*/
|
|
|
|
public function debug()
|
|
|
|
{
|
2017-05-17 07:40:13 +02:00
|
|
|
$class = static::class;
|
|
|
|
$val = "<h3>Database record: {$class}</h3>\n<ul>\n";
|
2016-11-29 00:31:16 +01:00
|
|
|
if ($this->record) {
|
|
|
|
foreach ($this->record as $fieldName => $fieldVal) {
|
|
|
|
$val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$val .= "</ul>\n";
|
|
|
|
return $val;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the DBField object that represents the given field.
|
|
|
|
* This works similarly to obj() with 2 key differences:
|
|
|
|
* - it still returns an object even when the field has no value.
|
|
|
|
* - it only matches fields and not methods
|
|
|
|
* - it matches foreign keys generated by has_one relationships, eg, "ParentID"
|
|
|
|
*
|
|
|
|
* @param string $fieldName Name of the field
|
|
|
|
* @return DBField The field as a DBField object
|
|
|
|
*/
|
|
|
|
public function dbObject($fieldName)
|
|
|
|
{
|
|
|
|
// Check for field in DB
|
|
|
|
$schema = static::getSchema();
|
|
|
|
$helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
|
|
|
|
if (!$helper) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2017-02-07 12:57:49 +01:00
|
|
|
if (!isset($this->record[$fieldName]) && isset($this->record[$fieldName . '_Lazy'])) {
|
|
|
|
$tableClass = $this->record[$fieldName . '_Lazy'];
|
|
|
|
$this->loadLazyFields($tableClass);
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
$value = isset($this->record[$fieldName])
|
|
|
|
? $this->record[$fieldName]
|
|
|
|
: null;
|
|
|
|
|
|
|
|
// If we have a DBField object in $this->record, then return that
|
|
|
|
if ($value instanceof DBField) {
|
|
|
|
return $value;
|
|
|
|
}
|
|
|
|
|
|
|
|
list($class, $spec) = explode('.', $helper);
|
|
|
|
/** @var DBField $obj */
|
|
|
|
$table = $schema->tableName($class);
|
2017-05-17 07:40:13 +02:00
|
|
|
$obj = Injector::inst()->create($spec, $fieldName);
|
2016-11-29 00:31:16 +01:00
|
|
|
$obj->setTable($table);
|
|
|
|
$obj->setValue($value, $this, false);
|
|
|
|
return $obj;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Traverses to a DBField referenced by relationships between data objects.
|
|
|
|
*
|
|
|
|
* The path to the related field is specified with dot separated syntax
|
|
|
|
* (eg: Parent.Child.Child.FieldName).
|
|
|
|
*
|
2017-11-09 05:08:31 +01:00
|
|
|
* If a relation is blank, this will return null instead.
|
|
|
|
* If a relation name is invalid (e.g. non-relation on a parent) this
|
|
|
|
* can throw a LogicException.
|
|
|
|
*
|
|
|
|
* @param string $fieldPath List of paths on this object. All items in this path
|
|
|
|
* must be ViewableData implementors
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
|
|
|
* @return mixed DBField of the field on the object or a DataList instance.
|
2017-11-09 05:08:31 +01:00
|
|
|
* @throws LogicException If accessing invalid relations
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function relObject($fieldPath)
|
|
|
|
{
|
|
|
|
$object = null;
|
2017-11-09 05:08:31 +01:00
|
|
|
$component = $this;
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-11-09 05:08:31 +01:00
|
|
|
// Parse all relations
|
|
|
|
foreach (explode('.', $fieldPath) as $relation) {
|
|
|
|
if (!$component) {
|
|
|
|
return null;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
2017-11-09 05:08:31 +01:00
|
|
|
// Inspect relation type
|
|
|
|
if (ClassInfo::hasMethod($component, $relation)) {
|
2018-01-30 06:28:28 +01:00
|
|
|
$component = $component->$relation();
|
2017-11-09 05:08:31 +01:00
|
|
|
} elseif ($component instanceof Relation || $component instanceof DataList) {
|
|
|
|
// $relation could either be a field (aggregate), or another relation
|
|
|
|
$singleton = DataObject::singleton($component->dataClass());
|
|
|
|
$component = $singleton->dbObject($relation) ?: $component->relation($relation);
|
|
|
|
} elseif ($component instanceof DataObject && ($dbObject = $component->dbObject($relation))) {
|
|
|
|
$component = $dbObject;
|
|
|
|
} elseif ($component instanceof ViewableData && $component->hasField($relation)) {
|
|
|
|
$component = $component->obj($relation);
|
|
|
|
} else {
|
|
|
|
throw new LogicException(
|
2018-01-22 02:57:05 +01:00
|
|
|
"$relation is not a relation/field on " . get_class($component)
|
2017-11-09 05:08:31 +01:00
|
|
|
);
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
2017-11-09 05:08:31 +01:00
|
|
|
return $component;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Traverses to a field referenced by relationships between data objects, returning the value
|
|
|
|
* The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
|
|
|
|
*
|
2017-11-09 05:08:31 +01:00
|
|
|
* @param string $fieldName string
|
|
|
|
* @return mixed Will return null on a missing value
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function relField($fieldName)
|
|
|
|
{
|
2017-11-09 05:08:31 +01:00
|
|
|
// Navigate to relative parent using relObject() if needed
|
2016-11-29 00:31:16 +01:00
|
|
|
$component = $this;
|
2017-11-09 05:08:31 +01:00
|
|
|
if (($pos = strrpos($fieldName, '.')) !== false) {
|
|
|
|
$relation = substr($fieldName, 0, $pos);
|
|
|
|
$fieldName = substr($fieldName, $pos + 1);
|
|
|
|
$component = $this->relObject($relation);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Bail if the component is null
|
|
|
|
if (!$component) {
|
|
|
|
return null;
|
|
|
|
}
|
2017-11-09 05:08:31 +01:00
|
|
|
if (ClassInfo::hasMethod($component, $fieldName)) {
|
2016-11-29 00:31:16 +01:00
|
|
|
return $component->$fieldName();
|
|
|
|
}
|
|
|
|
return $component->$fieldName;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Temporary hack to return an association name, based on class, to get around the mangle
|
|
|
|
* of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
|
|
|
|
*
|
|
|
|
* @param string $className
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getReverseAssociation($className)
|
|
|
|
{
|
|
|
|
if (is_array($this->manyMany())) {
|
|
|
|
$many_many = array_flip($this->manyMany());
|
|
|
|
if (array_key_exists($className, $many_many)) {
|
|
|
|
return $many_many[$className];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (is_array($this->hasMany())) {
|
|
|
|
$has_many = array_flip($this->hasMany());
|
|
|
|
if (array_key_exists($className, $has_many)) {
|
|
|
|
return $has_many[$className];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (is_array($this->hasOne())) {
|
|
|
|
$has_one = array_flip($this->hasOne());
|
|
|
|
if (array_key_exists($className, $has_one)) {
|
|
|
|
return $has_one[$className];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return all objects matching the filter
|
|
|
|
* sub-classes are automatically selected and included
|
|
|
|
*
|
|
|
|
* @param string $callerClass The class of objects to be returned
|
|
|
|
* @param string|array $filter A filter to be inserted into the WHERE clause.
|
|
|
|
* Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
|
|
|
|
* @param string|array $sort A sort expression to be inserted into the ORDER
|
|
|
|
* BY clause. If omitted, self::$default_sort will be used.
|
|
|
|
* @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
|
|
|
|
* @param string|array $limit A limit expression to be inserted into the LIMIT clause.
|
|
|
|
* @param string $containerClass The container class to return the results in.
|
|
|
|
*
|
|
|
|
* @todo $containerClass is Ignored, why?
|
|
|
|
*
|
|
|
|
* @return DataList The objects matching the filter, in the class specified by $containerClass
|
|
|
|
*/
|
|
|
|
public static function get(
|
|
|
|
$callerClass = null,
|
|
|
|
$filter = "",
|
|
|
|
$sort = "",
|
|
|
|
$join = "",
|
|
|
|
$limit = null,
|
|
|
|
$containerClass = DataList::class
|
|
|
|
) {
|
2018-02-28 22:53:28 +01:00
|
|
|
// Validate arguments
|
2016-11-29 00:31:16 +01:00
|
|
|
if ($callerClass == null) {
|
|
|
|
$callerClass = get_called_class();
|
2018-02-28 22:53:28 +01:00
|
|
|
if ($callerClass === self::class) {
|
|
|
|
throw new InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
2018-02-28 22:53:28 +01:00
|
|
|
if ($filter || $sort || $join || $limit || ($containerClass !== DataList::class)) {
|
|
|
|
throw new InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
|
2016-11-29 00:31:16 +01:00
|
|
|
. ' arguments');
|
|
|
|
}
|
2018-02-28 22:53:28 +01:00
|
|
|
} elseif ($callerClass === self::class) {
|
|
|
|
throw new InvalidArgumentException('DataObject::get() cannot query non-subclass DataObject directly');
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
if ($join) {
|
2018-02-28 22:53:28 +01:00
|
|
|
throw new InvalidArgumentException(
|
2016-11-29 00:31:16 +01:00
|
|
|
'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-02-28 22:53:28 +01:00
|
|
|
// Build and decorate with args
|
|
|
|
$result = DataList::create($callerClass);
|
|
|
|
if ($filter) {
|
|
|
|
$result = $result->where($filter);
|
|
|
|
}
|
|
|
|
if ($sort) {
|
|
|
|
$result = $result->sort($sort);
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
if ($limit && strpos($limit, ',') !== false) {
|
|
|
|
$limitArguments = explode(',', $limit);
|
|
|
|
$result = $result->limit($limitArguments[1], $limitArguments[0]);
|
|
|
|
} elseif ($limit) {
|
|
|
|
$result = $result->limit($limit);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the first item matching the given query.
|
2020-06-17 07:14:08 +02:00
|
|
|
*
|
|
|
|
* The object returned is cached, unlike DataObject::get()->first() {@link DataList::first()}
|
|
|
|
* and DataObject::get()->last() {@link DataList::last()}
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
2019-02-26 21:47:12 +01:00
|
|
|
* The filter argument supports parameterised queries (see SQLSelect::addWhere() for syntax examples). Because
|
|
|
|
* of that (and differently from e.g. DataList::filter()) you need to manually escape the field names:
|
|
|
|
* <code>
|
|
|
|
* $member = DataObject::get_one('Member', [ '"FirstName"' => 'John' ]);
|
|
|
|
* </code>
|
|
|
|
*
|
2016-11-29 00:31:16 +01:00
|
|
|
* @param string $callerClass The class of objects to be returned
|
|
|
|
* @param string|array $filter A filter to be inserted into the WHERE clause.
|
|
|
|
* @param boolean $cache Use caching
|
|
|
|
* @param string $orderby A sort expression to be inserted into the ORDER BY clause.
|
|
|
|
*
|
2017-06-29 02:26:40 +02:00
|
|
|
* @return DataObject|null The first item matching the query
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "")
|
|
|
|
{
|
2020-05-03 23:10:51 +02:00
|
|
|
/** @var DataObject $singleton */
|
|
|
|
$singleton = singleton($callerClass);
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2020-05-03 23:10:51 +02:00
|
|
|
$cacheComponents = [$filter, $orderby, $singleton->getUniqueKeyComponents()];
|
2017-11-25 17:56:50 +01:00
|
|
|
$cacheKey = md5(serialize($cacheComponents));
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
$item = null;
|
|
|
|
if (!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
|
|
|
|
$dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
|
|
|
|
$item = $dl->first();
|
|
|
|
|
|
|
|
if ($cache) {
|
|
|
|
self::$_cache_get_one[$callerClass][$cacheKey] = $item;
|
|
|
|
if (!self::$_cache_get_one[$callerClass][$cacheKey]) {
|
|
|
|
self::$_cache_get_one[$callerClass][$cacheKey] = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-06-29 02:26:40 +02:00
|
|
|
|
|
|
|
if ($cache) {
|
|
|
|
return self::$_cache_get_one[$callerClass][$cacheKey] ?: null;
|
|
|
|
}
|
2018-09-27 16:40:23 +02:00
|
|
|
|
|
|
|
return $item;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Flush the cached results for all relations (has_one, has_many, many_many)
|
|
|
|
* Also clears any cached aggregate data.
|
|
|
|
*
|
|
|
|
* @param boolean $persistent When true will also clear persistent data stored in the Cache system.
|
|
|
|
* When false will just clear session-local cached data
|
|
|
|
* @return DataObject $this
|
|
|
|
*/
|
|
|
|
public function flushCache($persistent = true)
|
|
|
|
{
|
2017-05-17 07:40:13 +02:00
|
|
|
if (static::class == self::class) {
|
2020-04-20 19:58:09 +02:00
|
|
|
self::$_cache_get_one = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2017-05-17 07:40:13 +02:00
|
|
|
$classes = ClassInfo::ancestry(static::class);
|
2016-11-29 00:31:16 +01:00
|
|
|
foreach ($classes as $class) {
|
|
|
|
if (isset(self::$_cache_get_one[$class])) {
|
|
|
|
unset(self::$_cache_get_one[$class]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->extend('flushCache');
|
|
|
|
|
2020-04-20 19:58:09 +02:00
|
|
|
$this->components = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Flush the get_one global cache and destroy associated objects.
|
|
|
|
*/
|
|
|
|
public static function flush_and_destroy_cache()
|
|
|
|
{
|
|
|
|
if (self::$_cache_get_one) {
|
|
|
|
foreach (self::$_cache_get_one as $class => $items) {
|
|
|
|
if (is_array($items)) {
|
|
|
|
foreach ($items as $item) {
|
|
|
|
if ($item) {
|
|
|
|
$item->destroy();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-04-20 19:58:09 +02:00
|
|
|
self::$_cache_get_one = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reset all global caches associated with DataObject.
|
|
|
|
*/
|
|
|
|
public static function reset()
|
|
|
|
{
|
|
|
|
// @todo Decouple these
|
2018-10-08 06:07:50 +02:00
|
|
|
DBEnum::flushCache();
|
2016-11-29 00:31:16 +01:00
|
|
|
ClassInfo::reset_db_cache();
|
|
|
|
static::getSchema()->reset();
|
2020-04-20 19:58:09 +02:00
|
|
|
self::$_cache_get_one = [];
|
|
|
|
self::$_cache_field_labels = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-02-28 22:53:28 +01:00
|
|
|
* Return the given element, searching by ID.
|
|
|
|
*
|
|
|
|
* This can be called either via `DataObject::get_by_id(MyClass::class, $id)`
|
|
|
|
* or `MyClass::get_by_id($id)`
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
2020-06-17 07:14:08 +02:00
|
|
|
* The object returned is cached, unlike DataObject::get()->byID() {@link DataList::byID()}
|
|
|
|
*
|
2018-02-28 22:53:28 +01:00
|
|
|
* @param string|int $classOrID The class of the object to be returned, or id if called on target class
|
|
|
|
* @param int|bool $idOrCache The id of the element, or cache if called on target class
|
|
|
|
* @param boolean $cache See {@link get_one()}
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
2018-02-28 14:25:28 +01:00
|
|
|
* @return static The element
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2018-02-28 22:53:28 +01:00
|
|
|
public static function get_by_id($classOrID, $idOrCache = null, $cache = true)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2018-02-28 22:53:28 +01:00
|
|
|
// Shift arguments if passing id in first or second argument
|
|
|
|
list ($class, $id, $cached) = is_numeric($classOrID)
|
|
|
|
? [get_called_class(), $classOrID, isset($idOrCache) ? $idOrCache : $cache]
|
|
|
|
: [$classOrID, $idOrCache, $cache];
|
2018-02-27 05:58:59 +01:00
|
|
|
|
2018-02-28 22:53:28 +01:00
|
|
|
// Validate class
|
|
|
|
if ($class === self::class) {
|
|
|
|
throw new InvalidArgumentException('DataObject::get_by_id() cannot query non-subclass DataObject directly');
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Pass to get_one
|
2018-02-28 22:53:28 +01:00
|
|
|
$column = static::getSchema()->sqlColumnForField($class, 'ID');
|
|
|
|
return DataObject::get_one($class, [$column => $id], $cached);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the name of the base table for this object
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function baseTable()
|
|
|
|
{
|
|
|
|
return static::getSchema()->baseDataTable($this);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the base class for this object
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function baseClass()
|
|
|
|
{
|
|
|
|
return static::getSchema()->baseDataClass($this);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array Parameters used in the query that built this object.
|
|
|
|
* This can be used by decorators (e.g. lazy loading) to
|
|
|
|
* run additional queries using the same context.
|
|
|
|
*/
|
|
|
|
protected $sourceQueryParams;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @see $sourceQueryParams
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getSourceQueryParams()
|
|
|
|
{
|
|
|
|
return $this->sourceQueryParams;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get list of parameters that should be inherited to relations on this object
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getInheritableQueryParams()
|
|
|
|
{
|
|
|
|
$params = $this->getSourceQueryParams();
|
|
|
|
$this->extend('updateInheritableQueryParams', $params);
|
|
|
|
return $params;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @see $sourceQueryParams
|
2020-12-21 22:23:23 +01:00
|
|
|
* @param array $array
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function setSourceQueryParams($array)
|
|
|
|
{
|
|
|
|
$this->sourceQueryParams = $array;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @see $sourceQueryParams
|
|
|
|
* @param string $key
|
|
|
|
* @param string $value
|
|
|
|
*/
|
|
|
|
public function setSourceQueryParam($key, $value)
|
|
|
|
{
|
|
|
|
$this->sourceQueryParams[$key] = $value;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @see $sourceQueryParams
|
|
|
|
* @param string $key
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getSourceQueryParam($key)
|
|
|
|
{
|
|
|
|
if (isset($this->sourceQueryParams[$key])) {
|
|
|
|
return $this->sourceQueryParams[$key];
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
//-------------------------------------------------------------------------------------------//
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check the database schema and update it as necessary.
|
|
|
|
*
|
2020-12-21 22:23:23 +01:00
|
|
|
* @uses DataExtension::augmentDatabase()
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function requireTable()
|
|
|
|
{
|
|
|
|
// Only build the table if we've actually got fields
|
|
|
|
$schema = static::getSchema();
|
|
|
|
$table = $schema->tableName(static::class);
|
2017-05-18 18:59:57 +02:00
|
|
|
$fields = $schema->databaseFields(static::class, false);
|
|
|
|
$indexes = $schema->databaseIndexes(static::class, false);
|
2016-11-29 00:31:16 +01:00
|
|
|
$extensions = self::database_extensions(static::class);
|
|
|
|
|
|
|
|
if (empty($table)) {
|
|
|
|
throw new LogicException(
|
|
|
|
"Class " . static::class . " not loaded by manifest, or no database table configured"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($fields) {
|
|
|
|
$hasAutoIncPK = get_parent_class($this) === self::class;
|
|
|
|
DB::require_table(
|
|
|
|
$table,
|
|
|
|
$fields,
|
|
|
|
$indexes,
|
|
|
|
$hasAutoIncPK,
|
2017-08-22 23:42:10 +02:00
|
|
|
$this->config()->get('create_table_options'),
|
2016-11-29 00:31:16 +01:00
|
|
|
$extensions
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
DB::dont_require_table($table);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Build any child tables for many_many items
|
|
|
|
if ($manyMany = $this->uninherited('many_many')) {
|
|
|
|
$extras = $this->uninherited('many_many_extraFields');
|
|
|
|
foreach ($manyMany as $component => $spec) {
|
|
|
|
// Get many_many spec
|
|
|
|
$manyManyComponent = $schema->manyManyComponent(static::class, $component);
|
2017-05-17 07:40:13 +02:00
|
|
|
$parentField = $manyManyComponent['parentField'];
|
|
|
|
$childField = $manyManyComponent['childField'];
|
|
|
|
$tableOrClass = $manyManyComponent['join'];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
// Skip if backed by actual class
|
|
|
|
if (class_exists($tableOrClass)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Build fields
|
2020-04-20 19:58:09 +02:00
|
|
|
$manymanyFields = [
|
2016-11-29 00:31:16 +01:00
|
|
|
$parentField => "Int",
|
|
|
|
$childField => "Int",
|
2020-04-20 19:58:09 +02:00
|
|
|
];
|
2016-11-29 00:31:16 +01:00
|
|
|
if (isset($extras[$component])) {
|
|
|
|
$manymanyFields = array_merge($manymanyFields, $extras[$component]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Build index list
|
2017-05-18 18:59:57 +02:00
|
|
|
$manymanyIndexes = [
|
|
|
|
$parentField => [
|
|
|
|
'type' => 'index',
|
|
|
|
'name' => $parentField,
|
|
|
|
'columns' => [$parentField],
|
|
|
|
],
|
|
|
|
$childField => [
|
|
|
|
'type' => 'index',
|
2018-01-30 06:28:28 +01:00
|
|
|
'name' => $childField,
|
2017-05-18 18:59:57 +02:00
|
|
|
'columns' => [$childField],
|
|
|
|
],
|
|
|
|
];
|
2016-11-29 00:31:16 +01:00
|
|
|
DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Let any extentions make their own database fields
|
|
|
|
$this->extend('augmentDatabase', $dummy);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add default records to database. This function is called whenever the
|
|
|
|
* database is built, after the database tables have all been created. Overload
|
|
|
|
* this to add default records when the database is built, but make sure you
|
|
|
|
* call parent::requireDefaultRecords().
|
|
|
|
*
|
2020-12-21 22:23:23 +01:00
|
|
|
* @uses DataExtension::requireDefaultRecords()
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function requireDefaultRecords()
|
|
|
|
{
|
2017-02-22 04:14:53 +01:00
|
|
|
$defaultRecords = $this->config()->uninherited('default_records');
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
if (!empty($defaultRecords)) {
|
2017-05-17 07:40:13 +02:00
|
|
|
$hasData = DataObject::get_one(static::class);
|
2016-11-29 00:31:16 +01:00
|
|
|
if (!$hasData) {
|
2017-05-17 07:40:13 +02:00
|
|
|
$className = static::class;
|
2016-11-29 00:31:16 +01:00
|
|
|
foreach ($defaultRecords as $record) {
|
2017-06-22 12:50:45 +02:00
|
|
|
$obj = Injector::inst()->create($className, $record);
|
2016-11-29 00:31:16 +01:00
|
|
|
$obj->write();
|
|
|
|
}
|
|
|
|
DB::alteration_message("Added default records to $className table", "created");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Let any extentions make their own database default data
|
|
|
|
$this->extend('requireDefaultRecords', $dummy);
|
|
|
|
}
|
|
|
|
|
2020-06-30 06:56:14 +02:00
|
|
|
/**
|
|
|
|
* Invoked after every database build is complete (including after table creation and
|
|
|
|
* default record population).
|
|
|
|
*
|
|
|
|
* See {@link DatabaseAdmin::doBuild()} for context.
|
|
|
|
*/
|
|
|
|
public function onAfterBuild()
|
|
|
|
{
|
|
|
|
$this->extend('onAfterBuild');
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* Get the default searchable fields for this object, as defined in the
|
|
|
|
* $searchable_fields list. If searchable fields are not defined on the
|
|
|
|
* data object, uses a default selection of summary fields.
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function searchableFields()
|
|
|
|
{
|
|
|
|
// can have mixed format, need to make consistent in most verbose form
|
2017-08-22 23:42:10 +02:00
|
|
|
$fields = $this->config()->get('searchable_fields');
|
2016-11-29 00:31:16 +01:00
|
|
|
$labels = $this->fieldLabels();
|
|
|
|
|
|
|
|
// fallback to summary fields (unless empty array is explicitly specified)
|
2018-01-30 06:28:28 +01:00
|
|
|
if (!$fields && !is_array($fields)) {
|
2016-11-29 00:31:16 +01:00
|
|
|
$summaryFields = array_keys($this->summaryFields());
|
2020-04-20 19:58:09 +02:00
|
|
|
$fields = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
// remove the custom getters as the search should not include them
|
|
|
|
$schema = static::getSchema();
|
|
|
|
if ($summaryFields) {
|
|
|
|
foreach ($summaryFields as $key => $name) {
|
|
|
|
$spec = $name;
|
|
|
|
|
|
|
|
// Extract field name in case this is a method called on a field (e.g. "Date.Nice")
|
|
|
|
if (($fieldPos = strpos($name, '.')) !== false) {
|
|
|
|
$name = substr($name, 0, $fieldPos);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($schema->fieldSpec($this, $name)) {
|
|
|
|
$fields[] = $name;
|
|
|
|
} elseif ($this->relObject($spec)) {
|
|
|
|
$fields[] = $spec;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// we need to make sure the format is unified before
|
|
|
|
// augmenting fields, so extensions can apply consistent checks
|
|
|
|
// but also after augmenting fields, because the extension
|
|
|
|
// might use the shorthand notation as well
|
|
|
|
|
|
|
|
// rewrite array, if it is using shorthand syntax
|
2020-04-20 19:58:09 +02:00
|
|
|
$rewrite = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
foreach ($fields as $name => $specOrName) {
|
|
|
|
$identifer = (is_int($name)) ? $specOrName : $name;
|
|
|
|
|
|
|
|
if (is_int($name)) {
|
|
|
|
// Format: array('MyFieldName')
|
2020-04-20 19:58:09 +02:00
|
|
|
$rewrite[$identifer] = [];
|
2017-11-09 05:08:31 +01:00
|
|
|
} elseif (is_array($specOrName) && ($relObject = $this->relObject($identifer))) {
|
2016-11-29 00:31:16 +01:00
|
|
|
// Format: array('MyFieldName' => array(
|
|
|
|
// 'filter => 'ExactMatchFilter',
|
|
|
|
// 'field' => 'NumericField', // optional
|
|
|
|
// 'title' => 'My Title', // optional
|
|
|
|
// ))
|
|
|
|
$rewrite[$identifer] = array_merge(
|
2020-04-20 19:58:09 +02:00
|
|
|
['filter' => $relObject->config()->get('default_search_filter_class')],
|
2016-11-29 00:31:16 +01:00
|
|
|
(array)$specOrName
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
// Format: array('MyFieldName' => 'ExactMatchFilter')
|
2020-04-20 19:58:09 +02:00
|
|
|
$rewrite[$identifer] = [
|
2016-11-29 00:31:16 +01:00
|
|
|
'filter' => $specOrName,
|
2020-04-20 19:58:09 +02:00
|
|
|
];
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
if (!isset($rewrite[$identifer]['title'])) {
|
|
|
|
$rewrite[$identifer]['title'] = (isset($labels[$identifer]))
|
|
|
|
? $labels[$identifer] : FormField::name_to_label($identifer);
|
|
|
|
}
|
|
|
|
if (!isset($rewrite[$identifer]['filter'])) {
|
|
|
|
/** @skipUpgrade */
|
|
|
|
$rewrite[$identifer]['filter'] = 'PartialMatchFilter';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$fields = $rewrite;
|
|
|
|
|
|
|
|
// apply DataExtensions if present
|
|
|
|
$this->extend('updateSearchableFields', $fields);
|
|
|
|
|
|
|
|
return $fields;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get any user defined searchable fields labels that
|
|
|
|
* exist. Allows overriding of default field names in the form
|
|
|
|
* interface actually presented to the user.
|
|
|
|
*
|
|
|
|
* The reason for keeping this separate from searchable_fields,
|
|
|
|
* which would be a logical place for this functionality, is to
|
|
|
|
* avoid bloating and complicating the configuration array. Currently
|
|
|
|
* much of this system is based on sensible defaults, and this property
|
|
|
|
* would generally only be set in the case of more complex relationships
|
|
|
|
* between data object being required in the search interface.
|
|
|
|
*
|
|
|
|
* Generates labels based on name of the field itself, if no static property
|
|
|
|
* {@link self::field_labels} exists.
|
|
|
|
*
|
|
|
|
* @uses $field_labels
|
|
|
|
* @uses FormField::name_to_label()
|
|
|
|
*
|
|
|
|
* @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
|
|
|
|
*
|
2018-07-04 00:55:20 +02:00
|
|
|
* @return array Array of all element labels
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function fieldLabels($includerelations = true)
|
|
|
|
{
|
2017-05-17 07:40:13 +02:00
|
|
|
$cacheKey = static::class . '_' . $includerelations;
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
if (!isset(self::$_cache_field_labels[$cacheKey])) {
|
2017-08-22 23:42:10 +02:00
|
|
|
$customLabels = $this->config()->get('field_labels');
|
2020-04-20 19:58:09 +02:00
|
|
|
$autoLabels = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
// get all translated static properties as defined in i18nCollectStatics()
|
2017-05-17 07:40:13 +02:00
|
|
|
$ancestry = ClassInfo::ancestry(static::class);
|
2016-11-29 00:31:16 +01:00
|
|
|
$ancestry = array_reverse($ancestry);
|
|
|
|
if ($ancestry) {
|
|
|
|
foreach ($ancestry as $ancestorClass) {
|
|
|
|
if ($ancestorClass === ViewableData::class) {
|
|
|
|
break;
|
|
|
|
}
|
2017-02-22 04:14:53 +01:00
|
|
|
$types = [
|
|
|
|
'db' => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
|
|
|
|
];
|
2016-11-29 00:31:16 +01:00
|
|
|
if ($includerelations) {
|
|
|
|
$types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
|
2018-01-30 06:28:28 +01:00
|
|
|
$types['has_many'] = (array)Config::inst()->get(
|
|
|
|
$ancestorClass,
|
|
|
|
'has_many',
|
|
|
|
Config::UNINHERITED
|
|
|
|
);
|
|
|
|
$types['many_many'] = (array)Config::inst()->get(
|
|
|
|
$ancestorClass,
|
|
|
|
'many_many',
|
|
|
|
Config::UNINHERITED
|
|
|
|
);
|
|
|
|
$types['belongs_many_many'] = (array)Config::inst()->get(
|
|
|
|
$ancestorClass,
|
|
|
|
'belongs_many_many',
|
|
|
|
Config::UNINHERITED
|
|
|
|
);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
foreach ($types as $type => $attrs) {
|
|
|
|
foreach ($attrs as $name => $spec) {
|
2018-01-30 06:28:28 +01:00
|
|
|
$autoLabels[$name] = _t(
|
|
|
|
"{$ancestorClass}.{$type}_{$name}",
|
|
|
|
FormField::name_to_label($name)
|
|
|
|
);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$labels = array_merge((array)$autoLabels, (array)$customLabels);
|
|
|
|
$this->extend('updateFieldLabels', $labels);
|
|
|
|
self::$_cache_field_labels[$cacheKey] = $labels;
|
|
|
|
}
|
|
|
|
|
|
|
|
return self::$_cache_field_labels[$cacheKey];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a human-readable label for a single field,
|
|
|
|
* see {@link fieldLabels()} for more details.
|
|
|
|
*
|
|
|
|
* @uses fieldLabels()
|
|
|
|
* @uses FormField::name_to_label()
|
|
|
|
*
|
|
|
|
* @param string $name Name of the field
|
|
|
|
* @return string Label of the field
|
|
|
|
*/
|
|
|
|
public function fieldLabel($name)
|
|
|
|
{
|
|
|
|
$labels = $this->fieldLabels();
|
|
|
|
return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the default summary fields for this object.
|
|
|
|
*
|
|
|
|
* @todo use the translation apparatus to return a default field selection for the language
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function summaryFields()
|
|
|
|
{
|
2017-12-14 02:17:19 +01:00
|
|
|
$rawFields = $this->config()->get('summary_fields');
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-12-14 02:17:19 +01:00
|
|
|
// Merge associative / numeric keys
|
|
|
|
$fields = [];
|
|
|
|
foreach ($rawFields as $key => $value) {
|
|
|
|
if (is_int($key)) {
|
|
|
|
$key = $value;
|
|
|
|
}
|
|
|
|
$fields[$key] = $value;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!$fields) {
|
2020-04-20 19:58:09 +02:00
|
|
|
$fields = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
// try to scaffold a couple of usual suspects
|
|
|
|
if ($this->hasField('Name')) {
|
|
|
|
$fields['Name'] = 'Name';
|
|
|
|
}
|
|
|
|
if (static::getSchema()->fieldSpec($this, 'Title')) {
|
|
|
|
$fields['Title'] = 'Title';
|
|
|
|
}
|
|
|
|
if ($this->hasField('Description')) {
|
|
|
|
$fields['Description'] = 'Description';
|
|
|
|
}
|
|
|
|
if ($this->hasField('FirstName')) {
|
|
|
|
$fields['FirstName'] = 'First Name';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$this->extend("updateSummaryFields", $fields);
|
|
|
|
|
|
|
|
// Final fail-over, just list ID field
|
|
|
|
if (!$fields) {
|
|
|
|
$fields['ID'] = 'ID';
|
|
|
|
}
|
|
|
|
|
|
|
|
// Localize fields (if possible)
|
|
|
|
foreach ($this->fieldLabels(false) as $name => $label) {
|
|
|
|
// only attempt to localize if the label definition is the same as the field name.
|
|
|
|
// this will preserve any custom labels set in the summary_fields configuration
|
|
|
|
if (isset($fields[$name]) && $name === $fields[$name]) {
|
|
|
|
$fields[$name] = $label;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $fields;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Defines a default list of filters for the search context.
|
|
|
|
*
|
|
|
|
* If a filter class mapping is defined on the data object,
|
|
|
|
* it is constructed here. Otherwise, the default filter specified in
|
|
|
|
* {@link DBField} is used.
|
|
|
|
*
|
|
|
|
* @todo error handling/type checking for valid FormField and SearchFilter subclasses?
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function defaultSearchFilters()
|
|
|
|
{
|
2020-04-20 19:58:09 +02:00
|
|
|
$filters = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
foreach ($this->searchableFields() as $name => $spec) {
|
|
|
|
if (empty($spec['filter'])) {
|
|
|
|
/** @skipUpgrade */
|
|
|
|
$filters[$name] = 'PartialMatchFilter';
|
|
|
|
} elseif ($spec['filter'] instanceof SearchFilter) {
|
|
|
|
$filters[$name] = $spec['filter'];
|
|
|
|
} else {
|
|
|
|
$filters[$name] = Injector::inst()->create($spec['filter'], $name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $filters;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return boolean True if the object is in the database
|
|
|
|
*/
|
|
|
|
public function isInDB()
|
|
|
|
{
|
|
|
|
return is_numeric($this->ID) && $this->ID > 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
2017-12-14 01:50:52 +01:00
|
|
|
* @ignore
|
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
private static $subclass_access = true;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Temporarily disable subclass access in data object qeur
|
|
|
|
*/
|
|
|
|
public static function disable_subclass_access()
|
|
|
|
{
|
|
|
|
self::$subclass_access = false;
|
|
|
|
}
|
2018-01-30 06:28:28 +01:00
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
public static function enable_subclass_access()
|
|
|
|
{
|
|
|
|
self::$subclass_access = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
//-------------------------------------------------------------------------------------------//
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Database field definitions.
|
|
|
|
* This is a map from field names to field type. The field
|
|
|
|
* type should be a class that extends .
|
|
|
|
* @var array
|
|
|
|
* @config
|
|
|
|
*/
|
2017-02-22 04:14:53 +01:00
|
|
|
private static $db = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Use a casting object for a field. This is a map from
|
|
|
|
* field name to class name of the casting object.
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
*/
|
2020-04-20 19:58:09 +02:00
|
|
|
private static $casting = [
|
2016-11-29 00:31:16 +01:00
|
|
|
"Title" => 'Text',
|
2020-04-20 19:58:09 +02:00
|
|
|
];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Specify custom options for a CREATE TABLE call.
|
|
|
|
* Can be used to specify a custom storage engine for specific database table.
|
|
|
|
* All options have to be keyed for a specific database implementation,
|
|
|
|
* identified by their class name (extending from {@link SS_Database}).
|
|
|
|
*
|
|
|
|
* <code>
|
|
|
|
* array(
|
|
|
|
* 'MySQLDatabase' => 'ENGINE=MyISAM'
|
|
|
|
* )
|
|
|
|
* </code>
|
|
|
|
*
|
|
|
|
* Caution: This API is experimental, and might not be
|
|
|
|
* included in the next major release. Please use with care.
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
* @config
|
|
|
|
*/
|
2020-04-20 19:58:09 +02:00
|
|
|
private static $create_table_options = [
|
2017-07-19 13:28:15 +02:00
|
|
|
MySQLSchemaManager::ID => 'ENGINE=InnoDB'
|
2020-04-20 19:58:09 +02:00
|
|
|
];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* If a field is in this array, then create a database index
|
|
|
|
* on that field. This is a map from fieldname to index type.
|
|
|
|
* See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation.
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
* @config
|
|
|
|
*/
|
|
|
|
private static $indexes = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Inserts standard column-values when a DataObject
|
2018-08-29 04:52:49 +02:00
|
|
|
* is instantiated. Does not insert default records {@see $default_records}.
|
2016-11-29 00:31:16 +01:00
|
|
|
* This is a map from fieldname to default value.
|
|
|
|
*
|
|
|
|
* - If you would like to change a default value in a sub-class, just specify it.
|
|
|
|
* - If you would like to disable the default value given by a parent class, set the default value to 0,'',
|
|
|
|
* or false in your subclass. Setting it to null won't work.
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
* @config
|
|
|
|
*/
|
2017-02-22 04:14:53 +01:00
|
|
|
private static $defaults = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Multidimensional array which inserts default data into the database
|
|
|
|
* on a db/build-call as long as the database-table is empty. Please use this only
|
|
|
|
* for simple constructs, not for SiteTree-Objects etc. which need special
|
|
|
|
* behaviour such as publishing and ParentNodes.
|
|
|
|
*
|
|
|
|
* Example:
|
|
|
|
* array(
|
|
|
|
* array('Title' => "DefaultPage1", 'PageTitle' => 'page1'),
|
|
|
|
* array('Title' => "DefaultPage2")
|
|
|
|
* ).
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
* @config
|
|
|
|
*/
|
|
|
|
private static $default_records = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* One-to-zero relationship defintion. This is a map of component name to data type. In order to turn this into a
|
|
|
|
* true one-to-one relationship you can add a {@link DataObject::$belongs_to} relationship on the child class.
|
|
|
|
*
|
|
|
|
* Note that you cannot have a has_one and belongs_to relationship with the same name.
|
|
|
|
*
|
2018-01-30 06:28:28 +01:00
|
|
|
* @var array
|
2016-11-29 00:31:16 +01:00
|
|
|
* @config
|
|
|
|
*/
|
2017-02-22 04:14:53 +01:00
|
|
|
private static $has_one = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}.
|
|
|
|
*
|
|
|
|
* This does not actually create any data structures, but allows you to query the other object in a one-to-one
|
|
|
|
* relationship from the child object. If you have multiple belongs_to links to another object you can use the
|
|
|
|
* syntax "ClassName.HasOneName" to specify which foreign has_one key on the other object to use.
|
|
|
|
*
|
|
|
|
* Note that you cannot have a has_one and belongs_to relationship with the same name.
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
* @config
|
|
|
|
*/
|
2017-02-22 04:14:53 +01:00
|
|
|
private static $belongs_to = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* This defines a one-to-many relationship. It is a map of component name to the remote data class.
|
|
|
|
*
|
|
|
|
* This relationship type does not actually create a data structure itself - you need to define a matching $has_one
|
|
|
|
* relationship on the child class. Also, if the $has_one relationship on the child class has multiple links to this
|
|
|
|
* class you can use the syntax "ClassName.HasOneRelationshipName" in the remote data class definition to show
|
|
|
|
* which foreign key to use.
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
* @config
|
|
|
|
*/
|
2017-02-22 04:14:53 +01:00
|
|
|
private static $has_many = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* many-many relationship definitions.
|
|
|
|
* This is a map from component name to data type.
|
|
|
|
* @var array
|
|
|
|
* @config
|
|
|
|
*/
|
2017-02-22 04:14:53 +01:00
|
|
|
private static $many_many = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Extra fields to include on the connecting many-many table.
|
|
|
|
* This is a map from field name to field type.
|
|
|
|
*
|
|
|
|
* Example code:
|
|
|
|
* <code>
|
|
|
|
* public static $many_many_extraFields = array(
|
|
|
|
* 'Members' => array(
|
|
|
|
* 'Role' => 'Varchar(100)'
|
|
|
|
* )
|
|
|
|
* );
|
|
|
|
* </code>
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
* @config
|
|
|
|
*/
|
2017-02-22 04:14:53 +01:00
|
|
|
private static $many_many_extraFields = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The inverse side of a many-many relationship.
|
|
|
|
* This is a map from component name to data type.
|
|
|
|
* @var array
|
|
|
|
* @config
|
|
|
|
*/
|
2017-02-22 04:14:53 +01:00
|
|
|
private static $belongs_many_many = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The default sort expression. This will be inserted in the ORDER BY
|
|
|
|
* clause of a SQL query if no other sort expression is provided.
|
|
|
|
* @var string
|
|
|
|
* @config
|
|
|
|
*/
|
|
|
|
private static $default_sort = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Default list of fields that can be scaffolded by the ModelAdmin
|
|
|
|
* search interface.
|
|
|
|
*
|
|
|
|
* Overriding the default filter, with a custom defined filter:
|
|
|
|
* <code>
|
|
|
|
* static $searchable_fields = array(
|
|
|
|
* "Name" => "PartialMatchFilter"
|
|
|
|
* );
|
|
|
|
* </code>
|
|
|
|
*
|
|
|
|
* Overriding the default form fields, with a custom defined field.
|
|
|
|
* The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
|
|
|
|
* The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
|
|
|
|
* <code>
|
|
|
|
* static $searchable_fields = array(
|
|
|
|
* "Name" => array(
|
|
|
|
* "field" => "TextField"
|
|
|
|
* )
|
|
|
|
* );
|
|
|
|
* </code>
|
|
|
|
*
|
|
|
|
* Overriding the default form field, filter and title:
|
|
|
|
* <code>
|
|
|
|
* static $searchable_fields = array(
|
|
|
|
* "Organisation.ZipCode" => array(
|
|
|
|
* "field" => "TextField",
|
|
|
|
* "filter" => "PartialMatchFilter",
|
|
|
|
* "title" => 'Organisation ZIP'
|
|
|
|
* )
|
|
|
|
* );
|
|
|
|
* </code>
|
|
|
|
* @config
|
2018-08-29 04:52:49 +02:00
|
|
|
* @var array
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
private static $searchable_fields = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* User defined labels for searchable_fields, used to override
|
|
|
|
* default display in the search form.
|
|
|
|
* @config
|
2018-08-29 04:52:49 +02:00
|
|
|
* @var array
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2017-02-22 04:14:53 +01:00
|
|
|
private static $field_labels = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Provides a default list of fields to be used by a 'summary'
|
|
|
|
* view of this object.
|
|
|
|
* @config
|
2018-08-29 04:52:49 +02:00
|
|
|
* @var array
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2017-02-22 04:14:53 +01:00
|
|
|
private static $summary_fields = [];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
public function provideI18nEntities()
|
|
|
|
{
|
2017-01-18 04:58:48 +01:00
|
|
|
// Note: see http://guides.rubyonrails.org/i18n.html#pluralization for rules
|
|
|
|
// Best guess for a/an rule. Better guesses require overriding in subclasses
|
|
|
|
$pluralName = $this->plural_name();
|
|
|
|
$singularName = $this->singular_name();
|
|
|
|
$conjunction = preg_match('/^[aeiou]/i', $singularName) ? 'An ' : 'A ';
|
|
|
|
return [
|
2018-01-16 19:39:30 +01:00
|
|
|
static::class . '.SINGULARNAME' => $this->singular_name(),
|
|
|
|
static::class . '.PLURALNAME' => $pluralName,
|
|
|
|
static::class . '.PLURALS' => [
|
2017-01-18 04:58:48 +01:00
|
|
|
'one' => $conjunction . $singularName,
|
|
|
|
'other' => '{count} ' . $pluralName
|
|
|
|
]
|
|
|
|
];
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the given method/parameter has a value
|
|
|
|
* (Uses the DBField::hasValue if the parameter is a database field)
|
|
|
|
*
|
|
|
|
* @param string $field The field name
|
|
|
|
* @param array $arguments
|
|
|
|
* @param bool $cache
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
public function hasValue($field, $arguments = null, $cache = true)
|
|
|
|
{
|
|
|
|
// has_one fields should not use dbObject to check if a value is given
|
|
|
|
$hasOne = static::getSchema()->hasOneComponent(static::class, $field);
|
|
|
|
if (!$hasOne && ($obj = $this->dbObject($field))) {
|
|
|
|
return $obj->exists();
|
|
|
|
} else {
|
|
|
|
return parent::hasValue($field, $arguments, $cache);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If selected through a many_many through relation, this is the instance of the joined record
|
|
|
|
*
|
|
|
|
* @return DataObject
|
|
|
|
*/
|
|
|
|
public function getJoin()
|
|
|
|
{
|
|
|
|
return $this->joinRecord;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set joining object
|
|
|
|
*
|
|
|
|
* @param DataObject $object
|
|
|
|
* @param string $alias Alias
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setJoin(DataObject $object, $alias = null)
|
|
|
|
{
|
|
|
|
$this->joinRecord = $object;
|
|
|
|
if ($alias) {
|
|
|
|
if (static::getSchema()->fieldSpec(static::class, $alias)) {
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
"Joined record $alias cannot also be a db field"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$this->record[$alias] = $object;
|
|
|
|
}
|
|
|
|
return $this;
|
|
|
|
}
|
2017-08-08 06:46:34 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Find objects in the given relationships, merging them into the given list
|
|
|
|
*
|
|
|
|
* @param string $source Config property to extract relationships from
|
|
|
|
* @param bool $recursive True if recursive
|
|
|
|
* @param ArrayList $list If specified, items will be added to this list. If not, a new
|
|
|
|
* instance of ArrayList will be constructed and returned
|
|
|
|
* @return ArrayList The list of related objects
|
|
|
|
*/
|
|
|
|
public function findRelatedObjects($source, $recursive = true, $list = null)
|
|
|
|
{
|
|
|
|
if (!$list) {
|
|
|
|
$list = new ArrayList();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Skip search for unsaved records
|
|
|
|
if (!$this->isInDB()) {
|
|
|
|
return $list;
|
|
|
|
}
|
|
|
|
|
|
|
|
$relationships = $this->config()->get($source) ?: [];
|
|
|
|
foreach ($relationships as $relationship) {
|
|
|
|
// Warn if invalid config
|
|
|
|
if (!$this->hasMethod($relationship)) {
|
|
|
|
trigger_error(sprintf(
|
|
|
|
"Invalid %s config value \"%s\" on object on class \"%s\"",
|
|
|
|
$source,
|
|
|
|
$relationship,
|
|
|
|
get_class($this)
|
|
|
|
), E_USER_WARNING);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Inspect value of this relationship
|
|
|
|
$items = $this->{$relationship}();
|
|
|
|
|
|
|
|
// Merge any new item
|
|
|
|
$newItems = $this->mergeRelatedObjects($list, $items);
|
|
|
|
|
|
|
|
// Recurse if necessary
|
|
|
|
if ($recursive) {
|
|
|
|
foreach ($newItems as $item) {
|
|
|
|
/** @var DataObject $item */
|
|
|
|
$item->findRelatedObjects($source, true, $list);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $list;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper method to merge owned/owning items into a list.
|
|
|
|
* Items already present in the list will be skipped.
|
|
|
|
*
|
|
|
|
* @param ArrayList $list Items to merge into
|
|
|
|
* @param mixed $items List of new items to merge
|
|
|
|
* @return ArrayList List of all newly added items that did not already exist in $list
|
|
|
|
*/
|
|
|
|
public function mergeRelatedObjects($list, $items)
|
|
|
|
{
|
|
|
|
$added = new ArrayList();
|
|
|
|
if (!$items) {
|
|
|
|
return $added;
|
|
|
|
}
|
|
|
|
if ($items instanceof DataObject) {
|
|
|
|
$items = [$items];
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @var DataObject $item */
|
|
|
|
foreach ($items as $item) {
|
|
|
|
$this->mergeRelatedObject($list, $added, $item);
|
|
|
|
}
|
|
|
|
return $added;
|
|
|
|
}
|
|
|
|
|
2020-05-03 23:10:51 +02:00
|
|
|
/**
|
|
|
|
* Generate a unique key for data object
|
|
|
|
* the unique key uses the @see DataObject::getUniqueKeyComponents() extension point so unique key modifiers
|
|
|
|
* such as versioned or fluent are covered
|
|
|
|
* i.e. same data object in different stages or different locales will produce different unique key
|
|
|
|
*
|
|
|
|
* recommended use:
|
|
|
|
* - when you need unique key for caching purposes
|
|
|
|
* - when you need unique id on the front end (for example JavaScript needs to target specific element)
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
* @throws Exception
|
|
|
|
*/
|
|
|
|
public function getUniqueKey(): string
|
|
|
|
{
|
|
|
|
/** @var UniqueKeyInterface $service */
|
|
|
|
$service = Injector::inst()->get(UniqueKeyInterface::class);
|
|
|
|
$keyComponents = $this->getUniqueKeyComponents();
|
|
|
|
|
|
|
|
return $service->generateKey($this, $keyComponents);
|
|
|
|
}
|
|
|
|
|
2017-08-08 06:46:34 +02:00
|
|
|
/**
|
|
|
|
* Merge single object into a list, but ensures that existing objects are not
|
|
|
|
* re-added.
|
|
|
|
*
|
|
|
|
* @param ArrayList $list Global list
|
|
|
|
* @param ArrayList $added Additional list to insert into
|
|
|
|
* @param DataObject $item Item to add
|
|
|
|
*/
|
|
|
|
protected function mergeRelatedObject($list, $added, $item)
|
|
|
|
{
|
|
|
|
// Identify item
|
|
|
|
$itemKey = get_class($item) . '/' . $item->ID;
|
|
|
|
|
|
|
|
// Write if saved, versioned, and not already added
|
|
|
|
if ($item->isInDB() && !isset($list[$itemKey])) {
|
|
|
|
$list[$itemKey] = $item;
|
|
|
|
$added[$itemKey] = $item;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add joined record (from many_many through) automatically
|
|
|
|
$joined = $item->getJoin();
|
|
|
|
if ($joined) {
|
|
|
|
$this->mergeRelatedObject($list, $added, $joined);
|
|
|
|
}
|
|
|
|
}
|
2020-05-03 23:10:51 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Extension point to add more cache key components.
|
|
|
|
* The framework extend method will return combined values from DataExtension method(s) as an array
|
|
|
|
* The method on your DataExtension class should return a single scalar value. For example:
|
|
|
|
*
|
|
|
|
* public function cacheKeyComponent()
|
|
|
|
* {
|
|
|
|
* return (string) $this->owner->MyColumn;
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
private function getUniqueKeyComponents(): array
|
|
|
|
{
|
|
|
|
return $this->extend('cacheKeyComponent');
|
|
|
|
}
|
2020-10-28 21:29:26 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Find all other DataObject instances that are related to this DataObject in the database
|
|
|
|
* through has_one and many_many relationships. For example:
|
|
|
|
* This method is called on a File. The MyPage model $has_one File. There is a Page record that has
|
|
|
|
* a FileID = $this->ID. This SS_List returned by this method will include that Page instance.
|
|
|
|
*
|
|
|
|
* @param string[] $excludedClasses
|
|
|
|
* @return SS_List
|
|
|
|
* @internal
|
|
|
|
*/
|
|
|
|
public function findAllRelatedData(array $excludedClasses = []): SS_List
|
|
|
|
{
|
|
|
|
$service = Injector::inst()->get(RelatedDataService::class);
|
|
|
|
return $service->findAll($this, $excludedClasses);
|
|
|
|
}
|
2012-03-27 06:04:11 +02:00
|
|
|
}
|