mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #5095 from open-sausages/pulls/4.0/ownership-api
API Ownership API Implementation
This commit is contained in:
commit
0f08176c6c
@ -129,6 +129,68 @@ is initialized. But it can also be set and reset temporarily to force a specific
|
||||
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records
|
||||
Versioned::set_reading_mode($origMode); // reset current mode
|
||||
|
||||
### File ownership
|
||||
|
||||
Typically when publishing versioned dataobjects, it is necessary to ensure that some linked components
|
||||
are published along with it. Unless this is done, site front-end content can appear incorrectly published.
|
||||
|
||||
For instance, a page which has a list of rotating banners will require that those banners are published
|
||||
whenever that page is.
|
||||
|
||||
The solution to this problem is the ownership API, which declares a two-way relationship between
|
||||
objects along database relations. This relationship is similar to many_many/belongs_many_many
|
||||
and has_one/has_many, however it relies on a pre-existing relationship to function.
|
||||
|
||||
For instance, in order to specify this dependency, you must apply `owns` and `owned_by` config
|
||||
on a relationship.
|
||||
|
||||
|
||||
:::php
|
||||
class MyPage extends Page {
|
||||
private static $has_many = array(
|
||||
'Banners' => 'Banner'
|
||||
);
|
||||
private static $owns = array(
|
||||
'Banners'
|
||||
);
|
||||
}
|
||||
|
||||
class Banner extends Page {
|
||||
private static $extensions = array(
|
||||
'Versioned'
|
||||
);
|
||||
private static $has_one = array(
|
||||
'Parent' => 'MyPage',
|
||||
'Image' => 'Image',
|
||||
);
|
||||
private static $owned_by = array(
|
||||
'Parent'
|
||||
);
|
||||
private static $owns = array(
|
||||
'Image'
|
||||
);
|
||||
}
|
||||
|
||||
class BannerImageExtension extends DataExtension {
|
||||
private static $has_many = array(
|
||||
'Banners' => 'Banner'
|
||||
);
|
||||
private static $owned_by = array(
|
||||
'Banners'
|
||||
);
|
||||
}
|
||||
|
||||
With the config:
|
||||
|
||||
:::yaml
|
||||
Image:
|
||||
extensions:
|
||||
- BannerImageExtension
|
||||
|
||||
|
||||
Note that it's important to define both `owns` and `owned_by` components of the relationship,
|
||||
similar to how you would apply `has_one` and `has_many`, or `many_many` and `belongs_many_many`.
|
||||
|
||||
### Custom SQL
|
||||
|
||||
We generally discourage writing `Versioned` queries from scratch, due to the complexities involved through joining
|
||||
|
@ -646,3 +646,9 @@ has been removed from core. You can configure the built-in `charmap` plugin inst
|
||||
|
||||
For more information on available options and plugins please refer to the
|
||||
[tinymce documentation](https://www.tinymce.com/docs/configure/)
|
||||
|
||||
### Implementation of ownership API
|
||||
|
||||
In order to support the recursive publishing of dataobjects, a new API has been developed to allow
|
||||
developers to declare dependencies between objects. See the
|
||||
[versioned documentation](/developer_guides/model/versioning) for more information.
|
||||
|
@ -158,7 +158,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
||||
*
|
||||
* @param string|array $keyOrArray Either the single key to set, or an array of key value pairs to set
|
||||
* @param mixed $val If $keyOrArray is not an array, this is the value to set
|
||||
* @return DataList
|
||||
* @return static
|
||||
*/
|
||||
public function setDataQueryParam($keyOrArray, $val = null) {
|
||||
$clone = clone $this;
|
||||
@ -179,7 +179,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
||||
* Returns the SQL query that will be used to get this DataList's records. Good for debugging. :-)
|
||||
*
|
||||
* @param array $parameters Out variable for parameters required for this query
|
||||
* @param string The resulting SQL query (may be paramaterised)
|
||||
* @return string The resulting SQL query (may be paramaterised)
|
||||
*/
|
||||
public function sql(&$parameters = array()) {
|
||||
return $this->dataQuery->query()->sql($parameters);
|
||||
@ -737,7 +737,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
||||
* @return DataObject
|
||||
*/
|
||||
protected function createDataObject($row) {
|
||||
$defaultClass = $this->dataClass;
|
||||
$class = $this->dataClass;
|
||||
|
||||
// Failover from RecordClassName to ClassName
|
||||
if(empty($row['RecordClassName'])) {
|
||||
@ -746,17 +746,26 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
||||
|
||||
// Instantiate the class mentioned in RecordClassName only if it exists, otherwise default to $this->dataClass
|
||||
if(class_exists($row['RecordClassName'])) {
|
||||
$item = Injector::inst()->create($row['RecordClassName'], $row, false, $this->model);
|
||||
} else {
|
||||
$item = Injector::inst()->create($defaultClass, $row, false, $this->model);
|
||||
$class = $row['RecordClassName'];
|
||||
}
|
||||
$item = Injector::inst()->create($class, $row, false, $this->model);
|
||||
|
||||
//set query params on the DataObject to tell the lazy loading mechanism the context the object creation context
|
||||
$item->setSourceQueryParams($this->dataQuery()->getQueryParams());
|
||||
$item->setSourceQueryParams($this->getQueryParams());
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query parameters for this list.
|
||||
* These values will be assigned as query parameters to newly created objects from this list.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getQueryParams() {
|
||||
return $this->dataQuery()->getQueryParams();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an Iterator for this DataList.
|
||||
* This function allows you to use DataLists in foreach loops
|
||||
|
@ -224,42 +224,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*/
|
||||
protected $unsavedRelations;
|
||||
|
||||
/**
|
||||
* Returns when validation on DataObjects is enabled.
|
||||
*
|
||||
* @deprecated 3.2 Use the "DataObject.validation_enabled" config setting instead
|
||||
* @return bool
|
||||
*/
|
||||
public static function get_validation_enabled() {
|
||||
Deprecation::notice('3.2', 'Use the "DataObject.validation_enabled" config setting instead');
|
||||
return Config::inst()->get('DataObject', 'validation_enabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether DataObjects should be validated before they are written.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @param $enable bool
|
||||
* @see DataObject::validate()
|
||||
* @deprecated 3.2 Use the "DataObject.validation_enabled" config setting instead
|
||||
*/
|
||||
public static function set_validation_enabled($enable) {
|
||||
Deprecation::notice('3.2', 'Use the "DataObject.validation_enabled" config setting instead');
|
||||
Config::inst()->update('DataObject', 'validation_enabled', (bool)$enable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached classname specs. It's necessary to clear all cached subclassed names
|
||||
* for any classes if a new class manifest is generated.
|
||||
*/
|
||||
public static function clear_classname_spec_cache() {
|
||||
Deprecation::notice('4.0', 'Call DBClassName::clear_classname_cache() instead');
|
||||
DBClassName::clear_classname_cache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the complete map of fields to specification on this object, including fixed_fields.
|
||||
* "ID" will be included on every table.
|
||||
@ -279,11 +243,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
// Refresh cache
|
||||
self::cache_database_fields($class);
|
||||
|
||||
|
||||
// Return cached values
|
||||
return self::$_cache_database_fields[$class];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cache all database and composite fields for the given class.
|
||||
* Will do nothing if already cached
|
||||
@ -309,7 +273,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
} else {
|
||||
$dbFields['ID'] = $fixedFields['ID'];
|
||||
}
|
||||
|
||||
|
||||
// Check each DB value as either a field or composite field
|
||||
$db = Config::inst()->get($class, 'db', Config::UNINHERITED) ?: array();
|
||||
foreach($db as $fieldName => $fieldSpec) {
|
||||
@ -366,7 +330,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
if(empty($class)) {
|
||||
$class = get_called_class();
|
||||
}
|
||||
|
||||
|
||||
// Get all fields
|
||||
$fields = self::database_fields($class);
|
||||
|
||||
@ -530,8 +494,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* Create a duplicate of this node.
|
||||
* Note: now also duplicates relations.
|
||||
*
|
||||
* @param $doWrite Perform a write() operation before returning the object. If this is true, it will create the
|
||||
* duplicate in the database.
|
||||
* @param bool $doWrite Perform a write() operation before returning the object.
|
||||
* If this is true, it will create the duplicate in the database.
|
||||
* @return DataObject A duplicate of this node. The exact type will be the type of this node.
|
||||
*/
|
||||
public function duplicate($doWrite = true) {
|
||||
@ -554,8 +518,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* The destinationObject must be written to the database already and have an ID. Writing is performed
|
||||
* automatically when adding the new relations.
|
||||
*
|
||||
* @param $sourceObject the source object to duplicate from
|
||||
* @param $destinationObject the destination object to populate with the duplicated relations
|
||||
* @param DataObject $sourceObject the source object to duplicate from
|
||||
* @param DataObject $destinationObject the destination object to populate with the duplicated relations
|
||||
* @return DataObject with the new many_many relations copied in
|
||||
*/
|
||||
protected function duplicateManyManyRelations($sourceObject, $destinationObject) {
|
||||
@ -1529,8 +1493,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* 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])) {
|
||||
@ -1548,28 +1512,38 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
|
||||
if($joinID) {
|
||||
$component = DataObject::get_by_id($class, $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)) {
|
||||
$component = $this->model->$class->newObject();
|
||||
}
|
||||
} elseif($class = $this->belongsToComponent($componentName)) {
|
||||
|
||||
$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
|
||||
$joinID = $this->ID;
|
||||
$joinID = $this->ID;
|
||||
|
||||
if($joinID) {
|
||||
|
||||
$filter = $polymorphic
|
||||
? array(
|
||||
// Prepare filter for appropriate join type
|
||||
if($polymorphic) {
|
||||
$filter = array(
|
||||
"{$joinField}ID" => $joinID,
|
||||
"{$joinField}Class" => $this->class
|
||||
)
|
||||
: array(
|
||||
);
|
||||
} else {
|
||||
$filter = array(
|
||||
$joinField => $joinID
|
||||
);
|
||||
$component = DataObject::get($class)->filter($filter)->first();
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
@ -1582,7 +1556,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Exception("DataObject->getComponent(): Could not find component '$componentName'.");
|
||||
throw new InvalidArgumentException(
|
||||
"DataObject->getComponent(): Could not find component '$componentName'."
|
||||
);
|
||||
}
|
||||
|
||||
$this->components[$componentName] = $component;
|
||||
@ -1593,31 +1569,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* Returns a one-to-many relation as a HasManyList
|
||||
*
|
||||
* @param string $componentName Name of the component
|
||||
* @param string|null $filter Deprecated. A filter to be inserted into the WHERE clause
|
||||
* @param string|null|array $sort Deprecated. A sort expression to be inserted into the ORDER BY clause. If omitted,
|
||||
* the static field $default_sort on the component class will be used.
|
||||
* @param string $join Deprecated, use leftJoin($table, $joinClause) instead
|
||||
* @param string|null|array $limit Deprecated. A limit expression to be inserted into the LIMIT clause
|
||||
*
|
||||
* @return HasManyList The components of the one-to-many relationship.
|
||||
*/
|
||||
public function getComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) {
|
||||
public function getComponents($componentName) {
|
||||
$result = null;
|
||||
|
||||
if(!$componentClass = $this->hasManyComponent($componentName)) {
|
||||
user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName'"
|
||||
. " on class '$this->class'", E_USER_ERROR);
|
||||
}
|
||||
|
||||
if($join) {
|
||||
throw new \InvalidArgumentException(
|
||||
'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
|
||||
);
|
||||
}
|
||||
|
||||
if($filter !== null || $sort !== null || $limit !== null) {
|
||||
Deprecation::notice('4.0', 'The $filter, $sort and $limit parameters for DataObject::getComponents()
|
||||
have been deprecated. Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
|
||||
$componentClass = $this->hasManyComponent($componentName);
|
||||
if(!$componentClass) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
"DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
|
||||
$componentName,
|
||||
$this->class
|
||||
));
|
||||
}
|
||||
|
||||
// If we haven't been written yet, we can't save these relations, so use a list that handles this case
|
||||
@ -1631,19 +1594,20 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
// Determine type and nature of foreign relation
|
||||
$joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic);
|
||||
/** @var HasManyList $result */
|
||||
if($polymorphic) {
|
||||
$result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
|
||||
} else {
|
||||
$result = HasManyList::create($componentClass, $joinField);
|
||||
}
|
||||
|
||||
if($this->model) $result->setDataModel($this->model);
|
||||
if($this->model) {
|
||||
$result->setDataModel($this->model);
|
||||
}
|
||||
|
||||
return $result
|
||||
->forForeignID($this->ID)
|
||||
->where($filter)
|
||||
->limit($limit)
|
||||
->sort($sort);
|
||||
->setDataQueryParam($this->getInheritableQueryParams())
|
||||
->forForeignID($this->ID);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1689,6 +1653,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @param string $type the join type - either 'has_many' or 'belongs_to'
|
||||
* @param boolean $polymorphic Flag set to true if the remote join field is polymorphic.
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
|
||||
// Extract relation from current object
|
||||
@ -1763,19 +1728,19 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* Returns a many-to-many component, as a ManyManyList.
|
||||
* @param string $componentName Name of the many-many component
|
||||
* @return ManyManyList The set of components
|
||||
*
|
||||
* @todo Implement query-params
|
||||
*/
|
||||
public function getManyManyComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) {
|
||||
list($parentClass, $componentClass, $parentField, $componentField, $table)
|
||||
= $this->manyManyComponent($componentName);
|
||||
|
||||
if($filter !== null || $sort !== null || $join !== null || $limit !== null) {
|
||||
Deprecation::notice('4.0', 'The $filter, $sort, $join and $limit parameters for
|
||||
DataObject::getManyManyComponents() have been deprecated.
|
||||
Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
|
||||
public function getManyManyComponents($componentName) {
|
||||
$manyManyComponent = $this->manyManyComponent($componentName);
|
||||
if(!$manyManyComponent) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
"DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
|
||||
$componentName,
|
||||
$this->class
|
||||
));
|
||||
}
|
||||
|
||||
list($parentClass, $componentClass, $parentField, $componentField, $table) = $manyManyComponent;
|
||||
|
||||
// If we haven't been written yet, we can't save these relations, so use a list that handles this case
|
||||
if(!$this->ID) {
|
||||
if(!isset($this->unsavedRelations[$componentName])) {
|
||||
@ -1786,54 +1751,29 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
|
||||
$extraFields = $this->manyManyExtraFieldsForComponent($componentName) ?: array();
|
||||
/** @var ManyManyList $result */
|
||||
$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
|
||||
|
||||
if($this->model) $result->setDataModel($this->model);
|
||||
if($this->model) {
|
||||
$result->setDataModel($this->model);
|
||||
}
|
||||
|
||||
$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
|
||||
->forForeignID($this->ID)
|
||||
->where($filter)
|
||||
->sort($sort)
|
||||
->limit($limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 4.0 Method has been replaced by hasOne() and hasOneComponent()
|
||||
* @param string $component
|
||||
* @return array|null
|
||||
*/
|
||||
public function has_one($component = null) {
|
||||
if($component) {
|
||||
Deprecation::notice('4.0', 'Please use hasOneComponent() instead');
|
||||
return $this->hasOneComponent($component);
|
||||
}
|
||||
|
||||
Deprecation::notice('4.0', 'Please use hasOne() instead');
|
||||
return $this->hasOne();
|
||||
->setDataQueryParam($this->getInheritableQueryParams())
|
||||
->forForeignID($this->ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param string $component Deprecated - Name of component
|
||||
* @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($component = null) {
|
||||
if($component) {
|
||||
Deprecation::notice(
|
||||
'4.0',
|
||||
'Please use DataObject::hasOneComponent() instead of passing a component name to hasOne()',
|
||||
Deprecation::SCOPE_GLOBAL
|
||||
);
|
||||
return $this->hasOneComponent($component);
|
||||
}
|
||||
|
||||
public function hasOne() {
|
||||
return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
|
||||
}
|
||||
|
||||
@ -1853,22 +1793,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 4.0 Method has been replaced by belongsTo() and belongsToComponent()
|
||||
* @param string $component
|
||||
* @param bool $classOnly
|
||||
* @return array|null
|
||||
*/
|
||||
public function belongs_to($component = null, $classOnly = true) {
|
||||
if($component) {
|
||||
Deprecation::notice('4.0', 'Please use belongsToComponent() instead');
|
||||
return $this->belongsToComponent($component, $classOnly);
|
||||
}
|
||||
|
||||
Deprecation::notice('4.0', 'Please use belongsTo() instead');
|
||||
return $this->belongsTo(null, $classOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@ -1964,22 +1888,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 4.0 Method has been replaced by hasMany() and hasManyComponent()
|
||||
* @param string $component
|
||||
* @param bool $classOnly
|
||||
* @return array|null
|
||||
*/
|
||||
public function has_many($component = null, $classOnly = true) {
|
||||
if($component) {
|
||||
Deprecation::notice('4.0', 'Please use hasManyComponent() instead');
|
||||
return $this->hasManyComponent($component, $classOnly);
|
||||
}
|
||||
|
||||
Deprecation::notice('4.0', 'Please use hasMany() instead');
|
||||
return $this->hasMany(null, $classOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@ -2026,42 +1934,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $hasMany) : $hasMany;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 4.0 Method has been replaced by manyManyExtraFields() and
|
||||
* manyManyExtraFieldsForComponent()
|
||||
* @param string $component
|
||||
* @return array
|
||||
*/
|
||||
public function many_many_extraFields($component = null) {
|
||||
if($component) {
|
||||
Deprecation::notice('4.0', 'Please use manyManyExtraFieldsForComponent() instead');
|
||||
return $this->manyManyExtraFieldsForComponent($component);
|
||||
}
|
||||
|
||||
Deprecation::notice('4.0', 'Please use manyManyExtraFields() instead');
|
||||
return $this->manyManyExtraFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param string $component Deprecated - Name of component
|
||||
* @return array|null
|
||||
*/
|
||||
public function manyManyExtraFields($component = null) {
|
||||
if($component) {
|
||||
Deprecation::notice(
|
||||
'4.0',
|
||||
'Please use DataObject::manyManyExtraFieldsForComponent() instead of passing a component name
|
||||
to manyManyExtraFields()',
|
||||
Deprecation::SCOPE_GLOBAL
|
||||
);
|
||||
return $this->manyManyExtraFieldsForComponent($component);
|
||||
}
|
||||
|
||||
public function manyManyExtraFields() {
|
||||
return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
|
||||
}
|
||||
|
||||
@ -2111,43 +1992,17 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
return isset($items) ? $items : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 4.0 Method has been renamed to manyMany()
|
||||
* @param string $component
|
||||
* @return array|null
|
||||
*/
|
||||
public function many_many($component = null) {
|
||||
if($component) {
|
||||
Deprecation::notice('4.0', 'Please use manyManyComponent() instead');
|
||||
return $this->manyManyComponent($component);
|
||||
}
|
||||
|
||||
Deprecation::notice('4.0', 'Please use manyMany() instead');
|
||||
return $this->manyMany();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 DataObject::manyManyComponent()
|
||||
* @param string $component Deprecated - Name of component
|
||||
* @return array|null An array of (parentclass, childclass), or an array of all many-many components
|
||||
*/
|
||||
public function manyMany($component = null) {
|
||||
if($component) {
|
||||
Deprecation::notice(
|
||||
'4.0',
|
||||
'Please use DataObject::manyManyComponent() instead of passing a component name to manyMany()',
|
||||
Deprecation::SCOPE_GLOBAL
|
||||
);
|
||||
return $this->manyManyComponent($component);
|
||||
}
|
||||
|
||||
public function manyMany() {
|
||||
$manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED);
|
||||
$belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
|
||||
|
||||
$items = array_merge($manyManys, $belongsManyManys);
|
||||
return $items;
|
||||
}
|
||||
@ -3285,6 +3140,17 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
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
|
||||
* @param array
|
||||
@ -3457,14 +3323,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$this->extend('requireDefaultRecords', $dummy);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 4.0
|
||||
*/
|
||||
public function inheritedDatabaseFields() {
|
||||
Deprecation::notice('4.0', 'Use db() instead');
|
||||
return $this->db();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default searchable fields for this object, as defined in the
|
||||
* $searchable_fields list. If searchable fields are not defined on the
|
||||
@ -3730,7 +3588,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
/**
|
||||
* Use a casting object for a field. This is a map from
|
||||
* field name to class name of the casting object.
|
||||
*
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $casting = array(
|
||||
|
@ -146,14 +146,6 @@ class ManyManyList extends RelationList {
|
||||
return $dataObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a filter expression for when getting the contents of the
|
||||
* relationship for some foreign ID
|
||||
*
|
||||
* @param int $id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function foreignIDFilter($id = null) {
|
||||
if ($id === null) {
|
||||
$id = $this->getForeignID();
|
||||
|
@ -16,11 +16,26 @@ abstract class RelationList extends DataList implements Relation {
|
||||
return $this->dataQuery->getQueryParam('Foreign.ID');
|
||||
}
|
||||
|
||||
public function getQueryParams() {
|
||||
$params = parent::getQueryParams();
|
||||
|
||||
// Remove `Foreign.` query parameters for created objects,
|
||||
// as this would interfere with relations on those objects.
|
||||
foreach(array_keys($params) as $key) {
|
||||
if(stripos($key, 'Foreign.') !== 0) {
|
||||
unset($params[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this list with the ManyMany relationship linked to
|
||||
* the given foreign ID.
|
||||
*
|
||||
* @param int|array $id An ID or an array of IDs.
|
||||
* @return static
|
||||
*/
|
||||
public function forForeignID($id) {
|
||||
// Turn a 1-element array into a simple value
|
||||
|
@ -8,6 +8,7 @@
|
||||
* the pages used in the CMS.
|
||||
*
|
||||
* @property int $Version
|
||||
* @property DataObject|Versioned $owner
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
@ -160,6 +161,34 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
*/
|
||||
private static $non_live_permissions = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain', 'VIEW_DRAFT_CONTENT');
|
||||
|
||||
/**
|
||||
* List of relationships on this object that are "owned" by this object.
|
||||
* Owership in the context of versioned objects is a relationship where
|
||||
* the publishing of owning objects requires the publishing of owned objects.
|
||||
*
|
||||
* E.g. A page owns a set of banners, as in order for the page to be published, all
|
||||
* banners on this page must also be published for it to be visible.
|
||||
*
|
||||
* Typically any object and its owned objects should be visible in the same edit view.
|
||||
* E.g. a page and {@see GridField} of banners.
|
||||
*
|
||||
* Page hierarchy is typically not considered an ownership relationship.
|
||||
*
|
||||
* Ownership is recursive; If A owns B and B owns C then A owns C.
|
||||
*
|
||||
* @config
|
||||
* @var array List of has_many or many_many relationships owned by this object.
|
||||
*/
|
||||
private static $owns = array();
|
||||
|
||||
/**
|
||||
* Opposing relationship to owns config; Represents the objects which
|
||||
* own the current object.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $owned_by = array();
|
||||
|
||||
/**
|
||||
* Reset static configuration variables to their default values.
|
||||
*/
|
||||
@ -201,14 +230,19 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
if($parts[0] == 'Archive') {
|
||||
$dataQuery->setQueryParam('Versioned.mode', 'archive');
|
||||
$dataQuery->setQueryParam('Versioned.date', $parts[1]);
|
||||
|
||||
} else if($parts[0] == 'Stage' && $parts[1] != $this->defaultStage
|
||||
&& array_search($parts[1],$this->stages) !== false) {
|
||||
|
||||
} else if($parts[0] == 'Stage' && in_array($parts[1], $this->stages)) {
|
||||
$dataQuery->setQueryParam('Versioned.mode', 'stage');
|
||||
$dataQuery->setQueryParam('Versioned.stage', $parts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function updateInheritableQueryParams(&$params) {
|
||||
// Versioned.mode === all_versions doesn't inherit very well, so default to stage
|
||||
if(isset($params['Versioned.mode']) && $params['Versioned.mode'] === 'all_versions') {
|
||||
$params['Versioned.mode'] = 'stage';
|
||||
$params['Versioned.stage'] = $this->defaultStage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -309,8 +343,12 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
case 'latest_versions':
|
||||
foreach($query->getFrom() as $alias => $join) {
|
||||
if($alias != $baseTable) {
|
||||
$query->setJoinFilter($alias, "\"$alias\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\""
|
||||
. " AND \"$alias\".\"Version\" = \"{$baseTable}_versions\".\"Version\"");
|
||||
// Make sure join includes version as well
|
||||
$query->setJoinFilter(
|
||||
$alias,
|
||||
"\"{$alias}_versions\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\""
|
||||
. " AND \"{$alias}_versions\".\"Version\" = \"{$baseTable}_versions\".\"Version\""
|
||||
);
|
||||
}
|
||||
$query->renameTable($alias, $alias . '_versions');
|
||||
}
|
||||
@ -320,33 +358,30 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name);
|
||||
}
|
||||
|
||||
// Alias the record ID as the row ID
|
||||
$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, 'RecordID'), "ID");
|
||||
// Alias the record ID as the row ID, and ensure ID filters are aliased correctly
|
||||
$query->selectField("\"{$baseTable}_versions\".\"RecordID\"", "ID");
|
||||
$query->replaceText("\"{$baseTable}_versions\".\"ID\"", "\"{$baseTable}_versions\".\"RecordID\"");
|
||||
|
||||
// Ensure that any sort order referring to this ID is correctly aliased
|
||||
$orders = $query->getOrderBy();
|
||||
foreach($orders as $order => $dir) {
|
||||
if($order === "\"$baseTable\".\"ID\"") {
|
||||
unset($orders[$order]);
|
||||
$orders["\"{$baseTable}_versions\".\"RecordID\""] = $dir;
|
||||
}
|
||||
}
|
||||
$query->setOrderBy($orders);
|
||||
// However, if doing count, undo rewrite of "ID" column
|
||||
$query->replaceText(
|
||||
"count(DISTINCT \"{$baseTable}_versions\".\"RecordID\")",
|
||||
"count(DISTINCT \"{$baseTable}_versions\".\"ID\")"
|
||||
);
|
||||
|
||||
// latest_version has one more step
|
||||
// Return latest version instances, regardless of whether they are on a particular stage
|
||||
// This provides "show all, including deleted" functonality
|
||||
if($dataQuery->getQueryParam('Versioned.mode') == 'latest_versions') {
|
||||
$query->addWhere(
|
||||
"\"{$alias}_versions\".\"Version\" IN
|
||||
"\"{$baseTable}_versions\".\"Version\" IN
|
||||
(SELECT LatestVersion FROM
|
||||
(SELECT
|
||||
\"{$alias}_versions\".\"RecordID\",
|
||||
MAX(\"{$alias}_versions\".\"Version\") AS LatestVersion
|
||||
FROM \"{$alias}_versions\"
|
||||
GROUP BY \"{$alias}_versions\".\"RecordID\"
|
||||
) AS \"{$alias}_versions_latest\"
|
||||
WHERE \"{$alias}_versions_latest\".\"RecordID\" = \"{$alias}_versions\".\"RecordID\"
|
||||
\"{$baseTable}_versions\".\"RecordID\",
|
||||
MAX(\"{$baseTable}_versions\".\"Version\") AS LatestVersion
|
||||
FROM \"{$baseTable}_versions\"
|
||||
GROUP BY \"{$baseTable}_versions\".\"RecordID\"
|
||||
) AS \"{$baseTable}_versions_latest\"
|
||||
WHERE \"{$baseTable}_versions_latest\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\"
|
||||
)");
|
||||
} else {
|
||||
// If all versions are requested, ensure that records are sorted by this field
|
||||
@ -415,14 +450,14 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
$versionableExtensions = $this->owner->config()->versionableExtensions;
|
||||
if(count($versionableExtensions)){
|
||||
foreach ($versionableExtensions as $versionableExtension => $suffixes) {
|
||||
if ($this->owner->hasExtension($versionableExtension)) {
|
||||
$allSuffixes = array_merge($allSuffixes, (array)$suffixes);
|
||||
foreach ((array)$suffixes as $suffix) {
|
||||
$allSuffixes[$suffix] = $versionableExtension;
|
||||
}
|
||||
if ($this->owner->hasExtension($versionableExtension)) {
|
||||
$allSuffixes = array_merge($allSuffixes, (array)$suffixes);
|
||||
foreach ((array)$suffixes as $suffix) {
|
||||
$allSuffixes[$suffix] = $versionableExtension;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the default table with an empty suffix to the list (table name = class name)
|
||||
array_push($allSuffixes,'');
|
||||
@ -779,6 +814,95 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
$this->migrateVersion(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all objects owned by the current object.
|
||||
* Note that objects will only be searched in the same stage as the given record.
|
||||
*
|
||||
* @param bool $recursive True if recursive
|
||||
* @param ArrayList $list Optional list to add items to
|
||||
* @return ArrayList list of objects
|
||||
*/
|
||||
public function findOwned($recursive = true, $list = null)
|
||||
{
|
||||
// Find objects in these relationships
|
||||
return $this->findRelatedObjects('owns', $recursive, $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find objects which own this object.
|
||||
* Note that objects will only be searched in the same stage as the given record.
|
||||
*
|
||||
* @param bool $recursive True if recursive
|
||||
* @param ArrayList $list Optional list to add items to
|
||||
* @return ArrayList list of objects
|
||||
*/
|
||||
public function findOwners($recursive = true, $list = null)
|
||||
{
|
||||
// Find objects in these relationships
|
||||
return $this->findRelatedObjects('owned_by', $recursive, $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find objects in the given relationships, merging them into the given list
|
||||
*
|
||||
* @param array $source Config property to extract relationships from
|
||||
* @param bool $recursive True if recursive
|
||||
* @param ArrayList $list Optional list to add items to
|
||||
* @return ArrayList The list
|
||||
*/
|
||||
public function findRelatedObjects($source, $recursive = true, $list = null)
|
||||
{
|
||||
if (!$list) {
|
||||
$list = new ArrayList();
|
||||
}
|
||||
|
||||
// Skip search for unsaved records
|
||||
if(!$this->owner->isInDB()) {
|
||||
return $list;
|
||||
}
|
||||
|
||||
$relationships = $this->owner->config()->{$source};
|
||||
foreach($relationships as $relationship) {
|
||||
// Warn if invalid config
|
||||
if(!$this->owner->hasMethod($relationship)) {
|
||||
trigger_error(sprintf(
|
||||
"Invalid %s config value \"%s\" on object on class \"%s\"",
|
||||
$source,
|
||||
$relationship,
|
||||
$this->owner->class
|
||||
), E_USER_WARNING);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Inspect value of this relationship
|
||||
$items = $this->owner->{$relationship}();
|
||||
if(!$items) {
|
||||
continue;
|
||||
}
|
||||
if($items instanceof DataObject) {
|
||||
$items = array($items);
|
||||
}
|
||||
|
||||
/** @var Versioned|DataObject $item */
|
||||
foreach($items as $item) {
|
||||
// Identify item
|
||||
$itemKey = $item->class . '/' . $item->ID;
|
||||
|
||||
// Skip unsaved, unversioned, or already checked objects
|
||||
if(!$item->isInDB() || !$item->has_extension('Versioned') || isset($list[$itemKey])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Save record
|
||||
$list[$itemKey] = $item;
|
||||
if($recursive) {
|
||||
$item->findRelatedObjects($source, true, $list);
|
||||
};
|
||||
}
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should return true if the current user can publish this record.
|
||||
* It can be overloaded to customise the security model for an application.
|
||||
@ -1020,11 +1144,11 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
if ($owner->hasExtension($versionableExtension)) {
|
||||
$ext = $owner->getExtensionInstance($versionableExtension);
|
||||
$ext->setOwner($owner);
|
||||
$table = $ext->extendWithSuffix($table);
|
||||
$ext->clearOwner();
|
||||
}
|
||||
$table = $ext->extendWithSuffix($table);
|
||||
$ext->clearOwner();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $table;
|
||||
}
|
||||
@ -1129,7 +1253,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
*/
|
||||
public function publish($fromStage, $toStage, $createNewVersion = false) {
|
||||
$owner = $this->owner;
|
||||
$owner->extend('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion);
|
||||
$owner->invokeWithExtensions('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion);
|
||||
|
||||
$baseClass = ClassInfo::baseDataClass($owner->class);
|
||||
|
||||
@ -1153,14 +1277,14 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
} else {
|
||||
$from->migrateVersion($from->Version);
|
||||
|
||||
// Mark this version as having been published at some stage
|
||||
// Mark this version as having been published at some stage
|
||||
$publisherID = isset(Member::currentUser()->ID) ? Member::currentUser()->ID : 0;
|
||||
$extTable = $this->extendWithSuffix($baseClass);
|
||||
DB::prepared_query("UPDATE \"{$extTable}_versions\"
|
||||
SET \"WasPublished\" = ?, \"PublisherID\" = ?
|
||||
WHERE \"RecordID\" = ? AND \"Version\" = ?",
|
||||
array(1, $publisherID, $from->ID, $from->Version)
|
||||
);
|
||||
DB::prepared_query("UPDATE \"{$extTable}_versions\"
|
||||
SET \"WasPublished\" = ?, \"PublisherID\" = ?
|
||||
WHERE \"RecordID\" = ? AND \"Version\" = ?",
|
||||
array(1, $publisherID, $from->ID, $from->Version)
|
||||
);
|
||||
}
|
||||
|
||||
// Change to new stage, write, and revert state
|
||||
@ -1184,7 +1308,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
|
||||
Versioned::set_reading_mode($oldMode);
|
||||
|
||||
$owner->extend('onAfterVersionedPublish', $fromStage, $toStage, $createNewVersion);
|
||||
$owner->invokeWithExtensions('onAfterVersionedPublish', $fromStage, $toStage, $createNewVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1245,6 +1369,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
* @param string $limit
|
||||
* @param string $join Deprecated, use leftJoin($table, $joinClause) instead
|
||||
* @param string $having
|
||||
* @return ArrayList
|
||||
*/
|
||||
public function allVersions($filter = "", $sort = "", $limit = "", $join = "", $having = "") {
|
||||
// Make sure the table names are not postfixed (e.g. _Live)
|
||||
@ -1310,6 +1435,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
/**
|
||||
* Return the base table - the class that directly extends DataObject.
|
||||
*
|
||||
* @param string $stage
|
||||
* @return string
|
||||
*/
|
||||
public function baseTable($stage = null) {
|
||||
@ -1476,7 +1602,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
* @param string $stage The name of the stage.
|
||||
* @param string $filter A filter to be inserted into the WHERE clause.
|
||||
* @param boolean $cache Use caching.
|
||||
* @param string $orderby A sort expression to be inserted into the ORDER BY clause.
|
||||
* @param string $sort A sort expression to be inserted into the ORDER BY clause.
|
||||
*
|
||||
* @return DataObject
|
||||
*/
|
||||
@ -1578,9 +1704,9 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
*
|
||||
* @return DataList A modified DataList designated to the specified stage
|
||||
*/
|
||||
public static function get_by_stage($class, $stage, $filter = '', $sort = '', $join = '', $limit = '',
|
||||
$containerClass = 'DataList') {
|
||||
|
||||
public static function get_by_stage(
|
||||
$class, $stage, $filter = '', $sort = '', $join = '', $limit = null, $containerClass = 'DataList'
|
||||
) {
|
||||
$result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass);
|
||||
return $result->setDataQueryParam(array(
|
||||
'Versioned.mode' => 'stage',
|
||||
@ -1589,27 +1715,28 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $stage
|
||||
* Delete this record from the given stage
|
||||
*
|
||||
* @return int
|
||||
* @param string $stage
|
||||
*/
|
||||
public function deleteFromStage($stage) {
|
||||
$oldMode = Versioned::get_reading_mode();
|
||||
Versioned::reading_stage($stage);
|
||||
$clone = clone $this->owner;
|
||||
$result = $clone->delete();
|
||||
$clone->delete();
|
||||
Versioned::set_reading_mode($oldMode);
|
||||
|
||||
// Fix the version number cache (in case you go delete from stage and then check ExistsOnLive)
|
||||
$baseClass = ClassInfo::baseDataClass($this->owner->class);
|
||||
self::$cache_versionnumber[$baseClass][$stage][$this->owner->ID] = null;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the given record to the draft stage
|
||||
*
|
||||
* @param string $stage
|
||||
* @param boolean $forceInsert
|
||||
* @return int The ID of the record
|
||||
*/
|
||||
public function writeToStage($stage, $forceInsert = false) {
|
||||
$oldMode = Versioned::get_reading_mode();
|
||||
@ -1639,12 +1766,14 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
/**
|
||||
* Return the latest version of the given record.
|
||||
*
|
||||
* @param string $class
|
||||
* @param int $id
|
||||
* @return DataObject
|
||||
*/
|
||||
public static function get_latest_version($class, $id) {
|
||||
$baseClass = ClassInfo::baseDataClass($class);
|
||||
$list = DataList::create($baseClass)
|
||||
->where("\"$baseClass\".\"RecordID\" = $id")
|
||||
->where(array("\"$baseClass\".\"RecordID\"" => $id))
|
||||
->setDataQueryParam("Versioned.mode", "latest_versions");
|
||||
|
||||
return $list->First();
|
||||
@ -1716,6 +1845,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
* @param string $class
|
||||
* @param string $filter
|
||||
* @param string $sort
|
||||
* @return DataList
|
||||
*/
|
||||
public static function get_including_deleted($class, $filter = "", $sort = "") {
|
||||
$list = DataList::create($class)
|
||||
@ -1742,8 +1872,10 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
public static function get_version($class, $id, $version) {
|
||||
$baseClass = ClassInfo::baseDataClass($class);
|
||||
$list = DataList::create($baseClass)
|
||||
->where("\"$baseClass\".\"RecordID\" = $id")
|
||||
->where("\"$baseClass\".\"Version\" = " . (int)$version)
|
||||
->where(array(
|
||||
"\"{$baseClass}\".\"RecordID\"" => $id,
|
||||
"\"{$baseClass}\".\"Version\"" => $version
|
||||
))
|
||||
->setDataQueryParam("Versioned.mode", 'all_versions');
|
||||
|
||||
return $list->First();
|
||||
@ -1758,9 +1890,8 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
* @return DataList
|
||||
*/
|
||||
public static function get_all_versions($class, $id) {
|
||||
$baseClass = ClassInfo::baseDataClass($class);
|
||||
$list = DataList::create($class)
|
||||
->where("\"$baseClass\".\"RecordID\" = $id")
|
||||
->filter('ID', $id)
|
||||
->setDataQueryParam('Versioned.mode', 'all_versions');
|
||||
|
||||
return $list;
|
||||
@ -1847,6 +1978,11 @@ class Versioned_Version extends ViewableData {
|
||||
*/
|
||||
protected $object;
|
||||
|
||||
/**
|
||||
* Create a new version from a database row
|
||||
*
|
||||
* @param array $record
|
||||
*/
|
||||
public function __construct($record) {
|
||||
$this->record = $record;
|
||||
$record['ID'] = $record['RecordID'];
|
||||
@ -1859,6 +1995,8 @@ class Versioned_Version extends ViewableData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Either 'published' if published, or 'internal' if not.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function PublishedClass() {
|
||||
@ -1866,6 +2004,8 @@ class Versioned_Version extends ViewableData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Author of this DataObject
|
||||
*
|
||||
* @return Member
|
||||
*/
|
||||
public function Author() {
|
||||
@ -1873,6 +2013,8 @@ class Versioned_Version extends ViewableData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Member object of the person who last published this record
|
||||
*
|
||||
* @return Member
|
||||
*/
|
||||
public function Publisher() {
|
||||
@ -1884,6 +2026,8 @@ class Versioned_Version extends ViewableData {
|
||||
}
|
||||
|
||||
/**
|
||||
* True if this record is published via publish() method
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function Published() {
|
||||
@ -1891,36 +2035,45 @@ class Versioned_Version extends ViewableData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied from DataObject to allow access via dot notation.
|
||||
* 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)
|
||||
*
|
||||
* @param $fieldName string
|
||||
* @return string | null - will return null on a missing value
|
||||
*/
|
||||
public function relField($fieldName) {
|
||||
$component = $this;
|
||||
|
||||
// We're dealing with relations here so we traverse the dot syntax
|
||||
if(strpos($fieldName, '.') !== false) {
|
||||
$parts = explode('.', $fieldName);
|
||||
$fieldName = array_pop($parts);
|
||||
|
||||
// Traverse dot syntax
|
||||
foreach($parts as $relation) {
|
||||
if($component instanceof SS_List) {
|
||||
if(method_exists($component,$relation)) {
|
||||
$relations = explode('.', $fieldName);
|
||||
$fieldName = array_pop($relations);
|
||||
foreach($relations as $relation) {
|
||||
// Inspect $component for element $relation
|
||||
if($component->hasMethod($relation)) {
|
||||
// Check nested method
|
||||
$component = $component->$relation();
|
||||
} else {
|
||||
} elseif($component instanceof SS_List) {
|
||||
// Select adjacent relation from DataList
|
||||
$component = $component->relation($relation);
|
||||
}
|
||||
} elseif($component instanceof DataObject
|
||||
&& ($dbObject = $component->dbObject($relation))
|
||||
) {
|
||||
// Select db object
|
||||
$component = $dbObject;
|
||||
} else {
|
||||
$component = $component->$relation();
|
||||
user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unlike has-one's, these "relations" can return false
|
||||
if($component) {
|
||||
// Bail if the component is null
|
||||
if(!$component) {
|
||||
return null;
|
||||
}
|
||||
if ($component->hasMethod($fieldName)) {
|
||||
return $component->$fieldName();
|
||||
}
|
||||
|
||||
return $component->$fieldName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ class DataDifferencerTest extends SapphireTest {
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
Versioned::reading_stage('Stage');
|
||||
|
||||
// Set backend root to /DataDifferencerTest
|
||||
AssetStoreTest_SpyStore::activate('DataDifferencerTest');
|
||||
|
||||
@ -39,11 +41,13 @@ class DataDifferencerTest extends SapphireTest {
|
||||
|
||||
public function testArrayValues() {
|
||||
$obj1 = $this->objFromFixture('DataDifferencerTest_Object', 'obj1');
|
||||
$beforeVersion = $obj1->Version;
|
||||
// create a new version
|
||||
$obj1->Choices = 'a';
|
||||
$obj1->write();
|
||||
$obj1v1 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $obj1->Version-1);
|
||||
$obj1v2 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $obj1->Version);
|
||||
$afterVersion = $obj1->Version;
|
||||
$obj1v1 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $beforeVersion);
|
||||
$obj1v2 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $afterVersion);
|
||||
$differ = new DataDifferencer($obj1v1, $obj1v2);
|
||||
$obj1Diff = $differ->diffedData();
|
||||
// TODO Using getter would split up field again, bug only caused by simulating
|
||||
@ -52,6 +56,7 @@ class DataDifferencerTest extends SapphireTest {
|
||||
}
|
||||
|
||||
public function testHasOnes() {
|
||||
/** @var DataDifferencerTest_Object $obj1 */
|
||||
$obj1 = $this->objFromFixture('DataDifferencerTest_Object', 'obj1');
|
||||
$image1 = $this->objFromFixture('Image', 'image1');
|
||||
$image2 = $this->objFromFixture('Image', 'image2');
|
||||
@ -59,14 +64,19 @@ class DataDifferencerTest extends SapphireTest {
|
||||
$relobj2 = $this->objFromFixture('DataDifferencerTest_HasOneRelationObject', 'relobj2');
|
||||
|
||||
// create a new version
|
||||
$beforeVersion = $obj1->Version;
|
||||
$obj1->ImageID = $image2->ID;
|
||||
$obj1->HasOneRelationID = $relobj2->ID;
|
||||
$obj1->write();
|
||||
$obj1v1 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $obj1->Version-1);
|
||||
$obj1v2 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $obj1->Version);
|
||||
$afterVersion = $obj1->Version;
|
||||
$this->assertNotEquals($beforeVersion, $afterVersion);
|
||||
/** @var DataDifferencerTest_Object $obj1v1 */
|
||||
$obj1v1 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $beforeVersion);
|
||||
/** @var DataDifferencerTest_Object $obj1v2 */
|
||||
$obj1v2 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $afterVersion);
|
||||
$differ = new DataDifferencer($obj1v1, $obj1v2);
|
||||
$obj1Diff = $differ->diffedData();
|
||||
|
||||
|
||||
$this->assertContains($image1->Name, $obj1Diff->getField('Image'));
|
||||
$this->assertContains($image2->Name, $obj1Diff->getField('Image'));
|
||||
$this->assertContains(
|
||||
@ -76,6 +86,11 @@ class DataDifferencerTest extends SapphireTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @property string $Choices
|
||||
* @method Image Image()
|
||||
* @method DataDifferencerTest_HasOneRelationObject HasOneRelation()
|
||||
*/
|
||||
class DataDifferencerTest_Object extends DataObject implements TestOnly {
|
||||
|
||||
private static $extensions = array('Versioned("Stage", "Live")');
|
||||
@ -113,4 +128,4 @@ class DataDifferencerTest_HasOneRelationObject extends DataObject implements Tes
|
||||
private static $has_many = array(
|
||||
'Objects' => 'DataDifferencerTest_Object'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
291
tests/model/VersionedOwnershipTest.php
Normal file
291
tests/model/VersionedOwnershipTest.php
Normal file
@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Tests ownership API of versioned DataObjects
|
||||
*/
|
||||
class VersionedOwnershipTest extends SapphireTest {
|
||||
|
||||
protected $extraDataObjects = array(
|
||||
'VersionedOwnershipTest_Object',
|
||||
'VersionedOwnershipTest_Subclass',
|
||||
'VersionedOwnershipTest_Related',
|
||||
'VersionedOwnershipTest_Attachment',
|
||||
);
|
||||
|
||||
protected static $fixture_file = 'VersionedOwnershipTest.yml';
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Versioned::reading_stage('Stage');
|
||||
|
||||
// Automatically publish any object named *_published
|
||||
foreach($this->getFixtureFactory()->getFixtures() as $class => $fixtures) {
|
||||
foreach($fixtures as $name => $id) {
|
||||
if(stripos($name, '_published') !== false) {
|
||||
/** @var Versioned|DataObject $object */
|
||||
$object = DataObject::get($class)->byID($id);
|
||||
$object->publish('Stage', 'Live');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test basic findOwned() in stage mode
|
||||
*/
|
||||
public function testFindOwned() {
|
||||
/** @var VersionedOwnershipTest_Subclass $subclass1 */
|
||||
$subclass1 = $this->objFromFixture('VersionedOwnershipTest_Subclass', 'subclass1_published');
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'Related 1'],
|
||||
['Title' => 'Attachment 1'],
|
||||
['Title' => 'Attachment 2'],
|
||||
['Title' => 'Attachment 5'],
|
||||
],
|
||||
$subclass1->findOwned()
|
||||
);
|
||||
|
||||
// Non-recursive search
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'Related 1'],
|
||||
],
|
||||
$subclass1->findOwned(false)
|
||||
);
|
||||
|
||||
/** @var VersionedOwnershipTest_Subclass $subclass2 */
|
||||
$subclass2 = $this->objFromFixture('VersionedOwnershipTest_Subclass', 'subclass2_published');
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'Related 2'],
|
||||
['Title' => 'Attachment 3'],
|
||||
['Title' => 'Attachment 4'],
|
||||
['Title' => 'Attachment 5'],
|
||||
],
|
||||
$subclass2->findOwned()
|
||||
);
|
||||
|
||||
// Non-recursive search
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'Related 2']
|
||||
],
|
||||
$subclass2->findOwned(false)
|
||||
);
|
||||
|
||||
/** @var VersionedOwnershipTest_Related $related1 */
|
||||
$related1 = $this->objFromFixture('VersionedOwnershipTest_Related', 'related1');
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'Attachment 1'],
|
||||
['Title' => 'Attachment 2'],
|
||||
['Title' => 'Attachment 5'],
|
||||
],
|
||||
$related1->findOwned()
|
||||
);
|
||||
|
||||
/** @var VersionedOwnershipTest_Related $related2 */
|
||||
$related2 = $this->objFromFixture('VersionedOwnershipTest_Related', 'related2_published');
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'Attachment 3'],
|
||||
['Title' => 'Attachment 4'],
|
||||
['Title' => 'Attachment 5'],
|
||||
],
|
||||
$related2->findOwned()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test findOwners
|
||||
*/
|
||||
public function testFindOwners() {
|
||||
/** @var VersionedOwnershipTest_Attachment $attachment1 */
|
||||
$attachment1 = $this->objFromFixture('VersionedOwnershipTest_Attachment', 'attachment1');
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'Related 1'],
|
||||
['Title' => 'Subclass 1'],
|
||||
],
|
||||
$attachment1->findOwners()
|
||||
);
|
||||
|
||||
// Non-recursive search
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'Related 1'],
|
||||
],
|
||||
$attachment1->findOwners(false)
|
||||
);
|
||||
|
||||
/** @var VersionedOwnershipTest_Attachment $attachment5 */
|
||||
$attachment5 = $this->objFromFixture('VersionedOwnershipTest_Attachment', 'attachment5_published');
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'Related 1'],
|
||||
['Title' => 'Related 2'],
|
||||
['Title' => 'Subclass 1'],
|
||||
['Title' => 'Subclass 2'],
|
||||
],
|
||||
$attachment5->findOwners()
|
||||
);
|
||||
|
||||
// Non-recursive
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'Related 1'],
|
||||
['Title' => 'Related 2'],
|
||||
],
|
||||
$attachment5->findOwners(false)
|
||||
);
|
||||
|
||||
/** @var VersionedOwnershipTest_Related $related1 */
|
||||
$related1 = $this->objFromFixture('VersionedOwnershipTest_Related', 'related1');
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'Subclass 1'],
|
||||
],
|
||||
$related1->findOwners()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test findOwners on Live stage
|
||||
*/
|
||||
public function testFindOwnersLive() {
|
||||
// Modify a few records on stage
|
||||
$related2 = $this->objFromFixture('VersionedOwnershipTest_Related', 'related2_published');
|
||||
$related2->Title .= ' Modified';
|
||||
$related2->write();
|
||||
$attachment3 = $this->objFromFixture('VersionedOwnershipTest_Attachment', 'attachment3_published');
|
||||
$attachment3->Title .= ' Modified';
|
||||
$attachment3->write();
|
||||
$attachment4 = $this->objFromFixture('VersionedOwnershipTest_Attachment', 'attachment4_published');
|
||||
$attachment4->delete();
|
||||
$subclass2ID = $this->idFromFixture('VersionedOwnershipTest_Subclass', 'subclass2_published');
|
||||
|
||||
// Check that stage record is ok
|
||||
/** @var VersionedOwnershipTest_Subclass $subclass2Stage */
|
||||
$subclass2Stage = \Versioned::get_by_stage('VersionedOwnershipTest_Subclass', 'Stage')->byID($subclass2ID);
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'Related 2 Modified'],
|
||||
['Title' => 'Attachment 3 Modified'],
|
||||
['Title' => 'Attachment 5'],
|
||||
],
|
||||
$subclass2Stage->findOwned()
|
||||
);
|
||||
|
||||
// Non-recursive
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'Related 2 Modified'],
|
||||
],
|
||||
$subclass2Stage->findOwned(false)
|
||||
);
|
||||
|
||||
// Live records are unchanged
|
||||
/** @var VersionedOwnershipTest_Subclass $subclass2Live */
|
||||
$subclass2Live = \Versioned::get_by_stage('VersionedOwnershipTest_Subclass', 'Live')->byID($subclass2ID);
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'Related 2'],
|
||||
['Title' => 'Attachment 3'],
|
||||
['Title' => 'Attachment 4'],
|
||||
['Title' => 'Attachment 5'],
|
||||
],
|
||||
$subclass2Live->findOwned()
|
||||
);
|
||||
|
||||
// Test non-recursive
|
||||
$this->assertDOSEquals(
|
||||
[
|
||||
['Title' => 'Related 2'],
|
||||
],
|
||||
$subclass2Live->findOwned(false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @mixin Versioned
|
||||
*/
|
||||
class VersionedOwnershipTest_Object extends DataObject implements TestOnly {
|
||||
private static $extensions = array(
|
||||
'Versioned',
|
||||
);
|
||||
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar(255)',
|
||||
'Content' => 'Text',
|
||||
);
|
||||
}
|
||||
|
||||
class VersionedOwnershipTest_Subclass extends VersionedOwnershipTest_Object implements TestOnly {
|
||||
private static $db = array(
|
||||
'Description' => 'Text',
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
'Related' => 'VersionedOwnershipTest_Related',
|
||||
);
|
||||
|
||||
private static $owns = array(
|
||||
'Related',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @mixin Versioned
|
||||
*/
|
||||
class VersionedOwnershipTest_Related extends DataObject implements TestOnly {
|
||||
private static $extensions = array(
|
||||
'Versioned',
|
||||
);
|
||||
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar(255)',
|
||||
);
|
||||
|
||||
private static $has_many = array(
|
||||
'Parents' => 'VersionedOwnershipTest_Subclass.Related',
|
||||
);
|
||||
|
||||
private static $owned_by = array(
|
||||
'Parents',
|
||||
);
|
||||
|
||||
private static $many_many = array(
|
||||
// Note : Currently unversioned, take care
|
||||
'Attachments' => 'VersionedOwnershipTest_Attachment',
|
||||
);
|
||||
|
||||
private static $owns = array(
|
||||
'Attachments',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @mixin Versioned
|
||||
*/
|
||||
class VersionedOwnershipTest_Attachment extends DataObject implements TestOnly {
|
||||
|
||||
private static $extensions = array(
|
||||
'Versioned',
|
||||
);
|
||||
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar(255)',
|
||||
);
|
||||
|
||||
private static $belongs_many_many = array(
|
||||
'AttachedTo' => 'VersionedOwnershipTest_Related.Attachments'
|
||||
);
|
||||
|
||||
private static $owned_by = array(
|
||||
'AttachedTo'
|
||||
);
|
||||
}
|
31
tests/model/VersionedOwnershipTest.yml
Normal file
31
tests/model/VersionedOwnershipTest.yml
Normal file
@ -0,0 +1,31 @@
|
||||
VersionedOwnershipTest_Attachment:
|
||||
attachment1:
|
||||
Title: 'Attachment 1'
|
||||
attachment2:
|
||||
Title: 'Attachment 2'
|
||||
attachment3_published:
|
||||
Title: 'Attachment 3'
|
||||
attachment4_published:
|
||||
Title: 'Attachment 4'
|
||||
attachment5_published:
|
||||
Title: 'Attachment 5'
|
||||
|
||||
VersionedOwnershipTest_Related:
|
||||
related1:
|
||||
Title: 'Related 1'
|
||||
Attachments: =>VersionedOwnershipTest_Attachment.attachment1,=>VersionedOwnershipTest_Attachment.attachment2,=>VersionedOwnershipTest_Attachment.attachment5_published
|
||||
related2_published:
|
||||
Title: 'Related 2'
|
||||
Attachments: =>VersionedOwnershipTest_Attachment.attachment3_published,=>VersionedOwnershipTest_Attachment.attachment4_published,=>VersionedOwnershipTest_Attachment.attachment5_published
|
||||
|
||||
VersionedOwnershipTest_Subclass:
|
||||
subclass1_published:
|
||||
Title: 'Subclass 1'
|
||||
Related: =>VersionedOwnershipTest_Related.related1
|
||||
subclass2_published:
|
||||
Title: 'Subclass 2'
|
||||
Related: =>VersionedOwnershipTest_Related.related2_published
|
||||
|
||||
VersionedOwnershipTest_Object:
|
||||
object1:
|
||||
Title: 'Object 1'
|
@ -522,7 +522,7 @@ class SecurityTest extends FunctionalTest {
|
||||
));
|
||||
$this->assertEquals($attempt->Status, 'Failure');
|
||||
$this->assertEquals($attempt->Email, 'sam@silverstripe.com');
|
||||
$this->assertEquals($attempt->Member(), $member);
|
||||
$this->assertEquals($attempt->MemberID, $member->ID);
|
||||
|
||||
/* UNSUCCESSFUL ATTEMPTS WITH NONEXISTING USER ARE LOGGED */
|
||||
$this->doTestLoginForm('wronguser@silverstripe.com', 'wrongpassword');
|
||||
@ -551,7 +551,7 @@ class SecurityTest extends FunctionalTest {
|
||||
$this->assertTrue(is_object($attempt));
|
||||
$this->assertEquals($attempt->Status, 'Success');
|
||||
$this->assertEquals($attempt->Email, 'sam@silverstripe.com');
|
||||
$this->assertEquals($attempt->Member(), $member);
|
||||
$this->assertEquals($attempt->MemberID, $member->ID);
|
||||
}
|
||||
|
||||
public function testDatabaseIsReadyWithInsufficientMemberColumns() {
|
||||
|
Loading…
Reference in New Issue
Block a user