API Ownership API

API Filter Relation. query parameters from relations when creating objects
API Versioned::publish now triggers invokeWithExtensions
API Update behaviour of versioned for all_versions mode to respect ID filters
API Tweak behaviour of inherited query parameters
This commit is contained in:
Damian Mooyman 2016-02-25 17:32:41 +13:00
parent adf356a6a5
commit b196d33bfa
11 changed files with 743 additions and 311 deletions

View File

@ -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 $obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records
Versioned::set_reading_mode($origMode); // reset current mode 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 ### Custom SQL
We generally discourage writing `Versioned` queries from scratch, due to the complexities involved through joining We generally discourage writing `Versioned` queries from scratch, due to the complexities involved through joining

View File

@ -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 For more information on available options and plugins please refer to the
[tinymce documentation](https://www.tinymce.com/docs/configure/) [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.

View File

@ -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 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 * @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) { public function setDataQueryParam($keyOrArray, $val = null) {
$clone = clone $this; $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. :-) * 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 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()) { public function sql(&$parameters = array()) {
return $this->dataQuery->query()->sql($parameters); return $this->dataQuery->query()->sql($parameters);
@ -737,7 +737,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
* @return DataObject * @return DataObject
*/ */
protected function createDataObject($row) { protected function createDataObject($row) {
$defaultClass = $this->dataClass; $class = $this->dataClass;
// Failover from RecordClassName to ClassName // Failover from RecordClassName to ClassName
if(empty($row['RecordClassName'])) { 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 // Instantiate the class mentioned in RecordClassName only if it exists, otherwise default to $this->dataClass
if(class_exists($row['RecordClassName'])) { if(class_exists($row['RecordClassName'])) {
$item = Injector::inst()->create($row['RecordClassName'], $row, false, $this->model); $class = $row['RecordClassName'];
} else {
$item = Injector::inst()->create($defaultClass, $row, false, $this->model);
} }
$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 //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; 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. * Returns an Iterator for this DataList.
* This function allows you to use DataLists in foreach loops * This function allows you to use DataLists in foreach loops

View File

@ -224,42 +224,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/ */
protected $unsavedRelations; 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. * Return the complete map of fields to specification on this object, including fixed_fields.
* "ID" will be included on every table. * "ID" will be included on every table.
@ -279,11 +243,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Refresh cache // Refresh cache
self::cache_database_fields($class); self::cache_database_fields($class);
// Return cached values // Return cached values
return self::$_cache_database_fields[$class]; return self::$_cache_database_fields[$class];
} }
/** /**
* Cache all database and composite fields for the given class. * Cache all database and composite fields for the given class.
* Will do nothing if already cached * Will do nothing if already cached
@ -309,7 +273,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} else { } else {
$dbFields['ID'] = $fixedFields['ID']; $dbFields['ID'] = $fixedFields['ID'];
} }
// Check each DB value as either a field or composite field // Check each DB value as either a field or composite field
$db = Config::inst()->get($class, 'db', Config::UNINHERITED) ?: array(); $db = Config::inst()->get($class, 'db', Config::UNINHERITED) ?: array();
foreach($db as $fieldName => $fieldSpec) { foreach($db as $fieldName => $fieldSpec) {
@ -366,7 +330,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if(empty($class)) { if(empty($class)) {
$class = get_called_class(); $class = get_called_class();
} }
// Get all fields // Get all fields
$fields = self::database_fields($class); $fields = self::database_fields($class);
@ -530,8 +494,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Create a duplicate of this node. * Create a duplicate of this node.
* Note: now also duplicates relations. * Note: now also duplicates relations.
* *
* @param $doWrite Perform a write() operation before returning the object. If this is true, it will create the * @param bool $doWrite Perform a write() operation before returning the object.
* duplicate in the database. * 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. * @return DataObject A duplicate of this node. The exact type will be the type of this node.
*/ */
public function duplicate($doWrite = true) { 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 * The destinationObject must be written to the database already and have an ID. Writing is performed
* automatically when adding the new relations. * automatically when adding the new relations.
* *
* @param $sourceObject the source object to duplicate from * @param DataObject $sourceObject the source object to duplicate from
* @param $destinationObject the destination object to populate with the duplicated relations * @param DataObject $destinationObject the destination object to populate with the duplicated relations
* @return DataObject with the new many_many relations copied in * @return DataObject with the new many_many relations copied in
*/ */
protected function duplicateManyManyRelations($sourceObject, $destinationObject) { 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. * non-polymorphic relations, or for polymorphic relations with a class set.
* *
* @param string $componentName Name of the component * @param string $componentName Name of the component
*
* @return DataObject The component object. It's exact type will be that of the component. * @return DataObject The component object. It's exact type will be that of the component.
* @throws Exception
*/ */
public function getComponent($componentName) { public function getComponent($componentName) {
if(isset($this->components[$componentName])) { if(isset($this->components[$componentName])) {
@ -1548,28 +1512,38 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
if($joinID) { 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)) { if(empty($component)) {
$component = $this->model->$class->newObject(); $component = $this->model->$class->newObject();
} }
} elseif($class = $this->belongsToComponent($componentName)) { } elseif($class = $this->belongsToComponent($componentName)) {
$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic); $joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
$joinID = $this->ID; $joinID = $this->ID;
if($joinID) { if($joinID) {
// Prepare filter for appropriate join type
$filter = $polymorphic if($polymorphic) {
? array( $filter = array(
"{$joinField}ID" => $joinID, "{$joinField}ID" => $joinID,
"{$joinField}Class" => $this->class "{$joinField}Class" => $this->class
) );
: array( } else {
$filter = array(
$joinField => $joinID $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)) { if(empty($component)) {
@ -1582,7 +1556,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
} }
} else { } 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; $this->components[$componentName] = $component;
@ -1593,31 +1569,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Returns a one-to-many relation as a HasManyList * Returns a one-to-many relation as a HasManyList
* *
* @param string $componentName Name of the component * @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. * @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; $result = null;
if(!$componentClass = $this->hasManyComponent($componentName)) { $componentClass = $this->hasManyComponent($componentName);
user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName'" if(!$componentClass) {
. " on class '$this->class'", E_USER_ERROR); throw new InvalidArgumentException(sprintf(
} "DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
$componentName,
if($join) { $this->class
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);
} }
// If we haven't been written yet, we can't save these relations, so use a list that handles this case // 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 // Determine type and nature of foreign relation
$joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic); $joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic);
/** @var HasManyList $result */
if($polymorphic) { if($polymorphic) {
$result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class); $result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
} else { } else {
$result = HasManyList::create($componentClass, $joinField); $result = HasManyList::create($componentClass, $joinField);
} }
if($this->model) $result->setDataModel($this->model); if($this->model) {
$result->setDataModel($this->model);
}
return $result return $result
->forForeignID($this->ID) ->setDataQueryParam($this->getInheritableQueryParams())
->where($filter) ->forForeignID($this->ID);
->limit($limit)
->sort($sort);
} }
/** /**
@ -1689,6 +1653,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* @param string $type the join type - either 'has_many' or 'belongs_to' * @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. * @param boolean $polymorphic Flag set to true if the remote join field is polymorphic.
* @return string * @return string
* @throws Exception
*/ */
public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) { public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
// Extract relation from current object // 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. * Returns a many-to-many component, as a ManyManyList.
* @param string $componentName Name of the many-many component * @param string $componentName Name of the many-many component
* @return ManyManyList The set of components * @return ManyManyList The set of components
*
* @todo Implement query-params
*/ */
public function getManyManyComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) { public function getManyManyComponents($componentName) {
list($parentClass, $componentClass, $parentField, $componentField, $table) $manyManyComponent = $this->manyManyComponent($componentName);
= $this->manyManyComponent($componentName); if(!$manyManyComponent) {
throw new InvalidArgumentException(sprintf(
if($filter !== null || $sort !== null || $join !== null || $limit !== null) { "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
Deprecation::notice('4.0', 'The $filter, $sort, $join and $limit parameters for $componentName,
DataObject::getManyManyComponents() have been deprecated. $this->class
Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL); ));
} }
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 we haven't been written yet, we can't save these relations, so use a list that handles this case
if(!$this->ID) { if(!$this->ID) {
if(!isset($this->unsavedRelations[$componentName])) { if(!isset($this->unsavedRelations[$componentName])) {
@ -1786,54 +1751,29 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
$extraFields = $this->manyManyExtraFieldsForComponent($componentName) ?: array(); $extraFields = $this->manyManyExtraFieldsForComponent($componentName) ?: array();
/** @var ManyManyList $result */
$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields); $result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
if($this->model) {
if($this->model) $result->setDataModel($this->model); $result->setDataModel($this->model);
}
$this->extend('updateManyManyComponents', $result); $this->extend('updateManyManyComponents', $result);
// If this is called on a singleton, then we return an 'orphaned relation' that can have the // If this is called on a singleton, then we return an 'orphaned relation' that can have the
// foreignID set elsewhere. // foreignID set elsewhere.
return $result return $result
->forForeignID($this->ID) ->setDataQueryParam($this->getInheritableQueryParams())
->where($filter) ->forForeignID($this->ID);
->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();
} }
/** /**
* Return the class of a one-to-one component. If $component is null, return all of the one-to-one components and * 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. * 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 * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
* their classes. * their classes.
*/ */
public function hasOne($component = null) { public function hasOne() {
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);
}
return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED); 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 * 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. * 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 * 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. * 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; 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. * Return the many-to-many extra fields specification.
* *
* If you don't specify a component name, it returns all * If you don't specify a component name, it returns all
* extra fields for all components available. * extra fields for all components available.
* *
* @param string $component Deprecated - Name of component
* @return array|null * @return array|null
*/ */
public function manyManyExtraFields($component = null) { public function manyManyExtraFields() {
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);
}
return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED); 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; 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. * 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 * The return value is an array of (parentclass, childclass). If $component is null, then all many-many
* components are returned. * components are returned.
* *
* @see DataObject::manyManyComponent() * @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 * @return array|null An array of (parentclass, childclass), or an array of all many-many components
*/ */
public function manyMany($component = null) { public function manyMany() {
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);
}
$manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED); $manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED);
$belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED); $belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
$items = array_merge($manyManys, $belongsManyManys); $items = array_merge($manyManys, $belongsManyManys);
return $items; return $items;
} }
@ -3285,6 +3140,17 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $this->sourceQueryParams; 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 * @see $sourceQueryParams
* @param array * @param array
@ -3457,14 +3323,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->extend('requireDefaultRecords', $dummy); $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 * Get the default searchable fields for this object, as defined in the
* $searchable_fields list. If searchable fields are not defined on 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 * Use a casting object for a field. This is a map from
* field name to class name of the casting object. * field name to class name of the casting object.
* *
* @var array * @var array
*/ */
private static $casting = array( private static $casting = array(

View File

@ -146,14 +146,6 @@ class ManyManyList extends RelationList {
return $dataObject; 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) { protected function foreignIDFilter($id = null) {
if ($id === null) { if ($id === null) {
$id = $this->getForeignID(); $id = $this->getForeignID();

View File

@ -16,11 +16,26 @@ abstract class RelationList extends DataList implements Relation {
return $this->dataQuery->getQueryParam('Foreign.ID'); 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 * Returns a copy of this list with the ManyMany relationship linked to
* the given foreign ID. * the given foreign ID.
* *
* @param int|array $id An ID or an array of IDs. * @param int|array $id An ID or an array of IDs.
* @return static
*/ */
public function forForeignID($id) { public function forForeignID($id) {
// Turn a 1-element array into a simple value // Turn a 1-element array into a simple value

View File

@ -8,6 +8,7 @@
* the pages used in the CMS. * the pages used in the CMS.
* *
* @property int $Version * @property int $Version
* @property DataObject|Versioned $owner
* *
* @package framework * @package framework
* @subpackage model * @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'); 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. * Reset static configuration variables to their default values.
*/ */
@ -201,14 +230,19 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
if($parts[0] == 'Archive') { if($parts[0] == 'Archive') {
$dataQuery->setQueryParam('Versioned.mode', 'archive'); $dataQuery->setQueryParam('Versioned.mode', 'archive');
$dataQuery->setQueryParam('Versioned.date', $parts[1]); $dataQuery->setQueryParam('Versioned.date', $parts[1]);
} else if($parts[0] == 'Stage' && in_array($parts[1], $this->stages)) {
} else if($parts[0] == 'Stage' && $parts[1] != $this->defaultStage
&& array_search($parts[1],$this->stages) !== false) {
$dataQuery->setQueryParam('Versioned.mode', 'stage'); $dataQuery->setQueryParam('Versioned.mode', 'stage');
$dataQuery->setQueryParam('Versioned.stage', $parts[1]); $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': case 'latest_versions':
foreach($query->getFrom() as $alias => $join) { foreach($query->getFrom() as $alias => $join) {
if($alias != $baseTable) { if($alias != $baseTable) {
$query->setJoinFilter($alias, "\"$alias\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\"" // Make sure join includes version as well
. " AND \"$alias\".\"Version\" = \"{$baseTable}_versions\".\"Version\""); $query->setJoinFilter(
$alias,
"\"{$alias}_versions\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\""
. " AND \"{$alias}_versions\".\"Version\" = \"{$baseTable}_versions\".\"Version\""
);
} }
$query->renameTable($alias, $alias . '_versions'); $query->renameTable($alias, $alias . '_versions');
} }
@ -320,33 +358,30 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name); $query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name);
} }
// Alias the record ID as the row ID // Alias the record ID as the row ID, and ensure ID filters are aliased correctly
$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, 'RecordID'), "ID"); $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 // However, if doing count, undo rewrite of "ID" column
$orders = $query->getOrderBy(); $query->replaceText(
foreach($orders as $order => $dir) { "count(DISTINCT \"{$baseTable}_versions\".\"RecordID\")",
if($order === "\"$baseTable\".\"ID\"") { "count(DISTINCT \"{$baseTable}_versions\".\"ID\")"
unset($orders[$order]); );
$orders["\"{$baseTable}_versions\".\"RecordID\""] = $dir;
}
}
$query->setOrderBy($orders);
// latest_version has one more step // latest_version has one more step
// Return latest version instances, regardless of whether they are on a particular stage // Return latest version instances, regardless of whether they are on a particular stage
// This provides "show all, including deleted" functonality // This provides "show all, including deleted" functonality
if($dataQuery->getQueryParam('Versioned.mode') == 'latest_versions') { if($dataQuery->getQueryParam('Versioned.mode') == 'latest_versions') {
$query->addWhere( $query->addWhere(
"\"{$alias}_versions\".\"Version\" IN "\"{$baseTable}_versions\".\"Version\" IN
(SELECT LatestVersion FROM (SELECT LatestVersion FROM
(SELECT (SELECT
\"{$alias}_versions\".\"RecordID\", \"{$baseTable}_versions\".\"RecordID\",
MAX(\"{$alias}_versions\".\"Version\") AS LatestVersion MAX(\"{$baseTable}_versions\".\"Version\") AS LatestVersion
FROM \"{$alias}_versions\" FROM \"{$baseTable}_versions\"
GROUP BY \"{$alias}_versions\".\"RecordID\" GROUP BY \"{$baseTable}_versions\".\"RecordID\"
) AS \"{$alias}_versions_latest\" ) AS \"{$baseTable}_versions_latest\"
WHERE \"{$alias}_versions_latest\".\"RecordID\" = \"{$alias}_versions\".\"RecordID\" WHERE \"{$baseTable}_versions_latest\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\"
)"); )");
} else { } else {
// If all versions are requested, ensure that records are sorted by this field // 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; $versionableExtensions = $this->owner->config()->versionableExtensions;
if(count($versionableExtensions)){ if(count($versionableExtensions)){
foreach ($versionableExtensions as $versionableExtension => $suffixes) { foreach ($versionableExtensions as $versionableExtension => $suffixes) {
if ($this->owner->hasExtension($versionableExtension)) { if ($this->owner->hasExtension($versionableExtension)) {
$allSuffixes = array_merge($allSuffixes, (array)$suffixes); $allSuffixes = array_merge($allSuffixes, (array)$suffixes);
foreach ((array)$suffixes as $suffix) { foreach ((array)$suffixes as $suffix) {
$allSuffixes[$suffix] = $versionableExtension; $allSuffixes[$suffix] = $versionableExtension;
}
} }
} }
} }
}
// Add the default table with an empty suffix to the list (table name = class name) // Add the default table with an empty suffix to the list (table name = class name)
array_push($allSuffixes,''); array_push($allSuffixes,'');
@ -779,6 +814,95 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
$this->migrateVersion(null); $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. * 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. * 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)) { if ($owner->hasExtension($versionableExtension)) {
$ext = $owner->getExtensionInstance($versionableExtension); $ext = $owner->getExtensionInstance($versionableExtension);
$ext->setOwner($owner); $ext->setOwner($owner);
$table = $ext->extendWithSuffix($table); $table = $ext->extendWithSuffix($table);
$ext->clearOwner(); $ext->clearOwner();
}
} }
} }
}
return $table; return $table;
} }
@ -1129,7 +1253,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
*/ */
public function publish($fromStage, $toStage, $createNewVersion = false) { public function publish($fromStage, $toStage, $createNewVersion = false) {
$owner = $this->owner; $owner = $this->owner;
$owner->extend('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion); $owner->invokeWithExtensions('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion);
$baseClass = ClassInfo::baseDataClass($owner->class); $baseClass = ClassInfo::baseDataClass($owner->class);
@ -1153,14 +1277,14 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
} else { } else {
$from->migrateVersion($from->Version); $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; $publisherID = isset(Member::currentUser()->ID) ? Member::currentUser()->ID : 0;
$extTable = $this->extendWithSuffix($baseClass); $extTable = $this->extendWithSuffix($baseClass);
DB::prepared_query("UPDATE \"{$extTable}_versions\" DB::prepared_query("UPDATE \"{$extTable}_versions\"
SET \"WasPublished\" = ?, \"PublisherID\" = ? SET \"WasPublished\" = ?, \"PublisherID\" = ?
WHERE \"RecordID\" = ? AND \"Version\" = ?", WHERE \"RecordID\" = ? AND \"Version\" = ?",
array(1, $publisherID, $from->ID, $from->Version) array(1, $publisherID, $from->ID, $from->Version)
); );
} }
// Change to new stage, write, and revert state // Change to new stage, write, and revert state
@ -1184,7 +1308,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
Versioned::set_reading_mode($oldMode); 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 $limit
* @param string $join Deprecated, use leftJoin($table, $joinClause) instead * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
* @param string $having * @param string $having
* @return ArrayList
*/ */
public function allVersions($filter = "", $sort = "", $limit = "", $join = "", $having = "") { public function allVersions($filter = "", $sort = "", $limit = "", $join = "", $having = "") {
// Make sure the table names are not postfixed (e.g. _Live) // 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. * Return the base table - the class that directly extends DataObject.
* *
* @param string $stage
* @return string * @return string
*/ */
public function baseTable($stage = null) { 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 $stage The name of the stage.
* @param string $filter A filter to be inserted into the WHERE clause. * @param string $filter A filter to be inserted into the WHERE clause.
* @param boolean $cache Use caching. * @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 * @return DataObject
*/ */
@ -1578,9 +1704,9 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
* *
* @return DataList A modified DataList designated to the specified stage * @return DataList A modified DataList designated to the specified stage
*/ */
public static function get_by_stage($class, $stage, $filter = '', $sort = '', $join = '', $limit = '', public static function get_by_stage(
$containerClass = 'DataList') { $class, $stage, $filter = '', $sort = '', $join = '', $limit = null, $containerClass = 'DataList'
) {
$result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass); $result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass);
return $result->setDataQueryParam(array( return $result->setDataQueryParam(array(
'Versioned.mode' => 'stage', '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) { public function deleteFromStage($stage) {
$oldMode = Versioned::get_reading_mode(); $oldMode = Versioned::get_reading_mode();
Versioned::reading_stage($stage); Versioned::reading_stage($stage);
$clone = clone $this->owner; $clone = clone $this->owner;
$result = $clone->delete(); $clone->delete();
Versioned::set_reading_mode($oldMode); Versioned::set_reading_mode($oldMode);
// Fix the version number cache (in case you go delete from stage and then check ExistsOnLive) // Fix the version number cache (in case you go delete from stage and then check ExistsOnLive)
$baseClass = ClassInfo::baseDataClass($this->owner->class); $baseClass = ClassInfo::baseDataClass($this->owner->class);
self::$cache_versionnumber[$baseClass][$stage][$this->owner->ID] = null; self::$cache_versionnumber[$baseClass][$stage][$this->owner->ID] = null;
return $result;
} }
/** /**
* Write the given record to the draft stage
*
* @param string $stage * @param string $stage
* @param boolean $forceInsert * @param boolean $forceInsert
* @return int The ID of the record
*/ */
public function writeToStage($stage, $forceInsert = false) { public function writeToStage($stage, $forceInsert = false) {
$oldMode = Versioned::get_reading_mode(); $oldMode = Versioned::get_reading_mode();
@ -1639,12 +1766,14 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
/** /**
* Return the latest version of the given record. * Return the latest version of the given record.
* *
* @param string $class
* @param int $id
* @return DataObject * @return DataObject
*/ */
public static function get_latest_version($class, $id) { public static function get_latest_version($class, $id) {
$baseClass = ClassInfo::baseDataClass($class); $baseClass = ClassInfo::baseDataClass($class);
$list = DataList::create($baseClass) $list = DataList::create($baseClass)
->where("\"$baseClass\".\"RecordID\" = $id") ->where(array("\"$baseClass\".\"RecordID\"" => $id))
->setDataQueryParam("Versioned.mode", "latest_versions"); ->setDataQueryParam("Versioned.mode", "latest_versions");
return $list->First(); return $list->First();
@ -1716,6 +1845,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
* @param string $class * @param string $class
* @param string $filter * @param string $filter
* @param string $sort * @param string $sort
* @return DataList
*/ */
public static function get_including_deleted($class, $filter = "", $sort = "") { public static function get_including_deleted($class, $filter = "", $sort = "") {
$list = DataList::create($class) $list = DataList::create($class)
@ -1742,8 +1872,10 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
public static function get_version($class, $id, $version) { public static function get_version($class, $id, $version) {
$baseClass = ClassInfo::baseDataClass($class); $baseClass = ClassInfo::baseDataClass($class);
$list = DataList::create($baseClass) $list = DataList::create($baseClass)
->where("\"$baseClass\".\"RecordID\" = $id") ->where(array(
->where("\"$baseClass\".\"Version\" = " . (int)$version) "\"{$baseClass}\".\"RecordID\"" => $id,
"\"{$baseClass}\".\"Version\"" => $version
))
->setDataQueryParam("Versioned.mode", 'all_versions'); ->setDataQueryParam("Versioned.mode", 'all_versions');
return $list->First(); return $list->First();
@ -1758,9 +1890,8 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
* @return DataList * @return DataList
*/ */
public static function get_all_versions($class, $id) { public static function get_all_versions($class, $id) {
$baseClass = ClassInfo::baseDataClass($class);
$list = DataList::create($class) $list = DataList::create($class)
->where("\"$baseClass\".\"RecordID\" = $id") ->filter('ID', $id)
->setDataQueryParam('Versioned.mode', 'all_versions'); ->setDataQueryParam('Versioned.mode', 'all_versions');
return $list; return $list;
@ -1847,6 +1978,11 @@ class Versioned_Version extends ViewableData {
*/ */
protected $object; protected $object;
/**
* Create a new version from a database row
*
* @param array $record
*/
public function __construct($record) { public function __construct($record) {
$this->record = $record; $this->record = $record;
$record['ID'] = $record['RecordID']; $record['ID'] = $record['RecordID'];
@ -1859,6 +1995,8 @@ class Versioned_Version extends ViewableData {
} }
/** /**
* Either 'published' if published, or 'internal' if not.
*
* @return string * @return string
*/ */
public function PublishedClass() { public function PublishedClass() {
@ -1866,6 +2004,8 @@ class Versioned_Version extends ViewableData {
} }
/** /**
* Author of this DataObject
*
* @return Member * @return Member
*/ */
public function Author() { public function Author() {
@ -1873,6 +2013,8 @@ class Versioned_Version extends ViewableData {
} }
/** /**
* Member object of the person who last published this record
*
* @return Member * @return Member
*/ */
public function Publisher() { public function Publisher() {
@ -1884,6 +2026,8 @@ class Versioned_Version extends ViewableData {
} }
/** /**
* True if this record is published via publish() method
*
* @return boolean * @return boolean
*/ */
public function Published() { 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) { public function relField($fieldName) {
$component = $this; $component = $this;
// We're dealing with relations here so we traverse the dot syntax
if(strpos($fieldName, '.') !== false) { if(strpos($fieldName, '.') !== false) {
$parts = explode('.', $fieldName); $relations = explode('.', $fieldName);
$fieldName = array_pop($parts); $fieldName = array_pop($relations);
foreach($relations as $relation) {
// Traverse dot syntax // Inspect $component for element $relation
foreach($parts as $relation) { if($component->hasMethod($relation)) {
if($component instanceof SS_List) { // Check nested method
if(method_exists($component,$relation)) {
$component = $component->$relation(); $component = $component->$relation();
} else { } elseif($component instanceof SS_List) {
// Select adjacent relation from DataList
$component = $component->relation($relation); $component = $component->relation($relation);
} } elseif($component instanceof DataObject
&& ($dbObject = $component->dbObject($relation))
) {
// Select db object
$component = $dbObject;
} else { } 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 // Bail if the component is null
if($component) { if(!$component) {
return null;
}
if ($component->hasMethod($fieldName)) { if ($component->hasMethod($fieldName)) {
return $component->$fieldName(); return $component->$fieldName();
} }
return $component->$fieldName; return $component->$fieldName;
} }
} }
}

View File

@ -19,6 +19,8 @@ class DataDifferencerTest extends SapphireTest {
public function setUp() { public function setUp() {
parent::setUp(); parent::setUp();
Versioned::reading_stage('Stage');
// Set backend root to /DataDifferencerTest // Set backend root to /DataDifferencerTest
AssetStoreTest_SpyStore::activate('DataDifferencerTest'); AssetStoreTest_SpyStore::activate('DataDifferencerTest');
@ -39,11 +41,13 @@ class DataDifferencerTest extends SapphireTest {
public function testArrayValues() { public function testArrayValues() {
$obj1 = $this->objFromFixture('DataDifferencerTest_Object', 'obj1'); $obj1 = $this->objFromFixture('DataDifferencerTest_Object', 'obj1');
$beforeVersion = $obj1->Version;
// create a new version // create a new version
$obj1->Choices = 'a'; $obj1->Choices = 'a';
$obj1->write(); $obj1->write();
$obj1v1 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $obj1->Version-1); $afterVersion = $obj1->Version;
$obj1v2 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $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); $differ = new DataDifferencer($obj1v1, $obj1v2);
$obj1Diff = $differ->diffedData(); $obj1Diff = $differ->diffedData();
// TODO Using getter would split up field again, bug only caused by simulating // TODO Using getter would split up field again, bug only caused by simulating
@ -52,6 +56,7 @@ class DataDifferencerTest extends SapphireTest {
} }
public function testHasOnes() { public function testHasOnes() {
/** @var DataDifferencerTest_Object $obj1 */
$obj1 = $this->objFromFixture('DataDifferencerTest_Object', 'obj1'); $obj1 = $this->objFromFixture('DataDifferencerTest_Object', 'obj1');
$image1 = $this->objFromFixture('Image', 'image1'); $image1 = $this->objFromFixture('Image', 'image1');
$image2 = $this->objFromFixture('Image', 'image2'); $image2 = $this->objFromFixture('Image', 'image2');
@ -59,14 +64,19 @@ class DataDifferencerTest extends SapphireTest {
$relobj2 = $this->objFromFixture('DataDifferencerTest_HasOneRelationObject', 'relobj2'); $relobj2 = $this->objFromFixture('DataDifferencerTest_HasOneRelationObject', 'relobj2');
// create a new version // create a new version
$beforeVersion = $obj1->Version;
$obj1->ImageID = $image2->ID; $obj1->ImageID = $image2->ID;
$obj1->HasOneRelationID = $relobj2->ID; $obj1->HasOneRelationID = $relobj2->ID;
$obj1->write(); $obj1->write();
$obj1v1 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $obj1->Version-1); $afterVersion = $obj1->Version;
$obj1v2 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $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); $differ = new DataDifferencer($obj1v1, $obj1v2);
$obj1Diff = $differ->diffedData(); $obj1Diff = $differ->diffedData();
$this->assertContains($image1->Name, $obj1Diff->getField('Image')); $this->assertContains($image1->Name, $obj1Diff->getField('Image'));
$this->assertContains($image2->Name, $obj1Diff->getField('Image')); $this->assertContains($image2->Name, $obj1Diff->getField('Image'));
$this->assertContains( $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 { class DataDifferencerTest_Object extends DataObject implements TestOnly {
private static $extensions = array('Versioned("Stage", "Live")'); private static $extensions = array('Versioned("Stage", "Live")');
@ -113,4 +128,4 @@ class DataDifferencerTest_HasOneRelationObject extends DataObject implements Tes
private static $has_many = array( private static $has_many = array(
'Objects' => 'DataDifferencerTest_Object' 'Objects' => 'DataDifferencerTest_Object'
); );
} }

View 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'
);
}

View 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'

View File

@ -522,7 +522,7 @@ class SecurityTest extends FunctionalTest {
)); ));
$this->assertEquals($attempt->Status, 'Failure'); $this->assertEquals($attempt->Status, 'Failure');
$this->assertEquals($attempt->Email, 'sam@silverstripe.com'); $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 */ /* UNSUCCESSFUL ATTEMPTS WITH NONEXISTING USER ARE LOGGED */
$this->doTestLoginForm('wronguser@silverstripe.com', 'wrongpassword'); $this->doTestLoginForm('wronguser@silverstripe.com', 'wrongpassword');
@ -551,7 +551,7 @@ class SecurityTest extends FunctionalTest {
$this->assertTrue(is_object($attempt)); $this->assertTrue(is_object($attempt));
$this->assertEquals($attempt->Status, 'Success'); $this->assertEquals($attempt->Status, 'Success');
$this->assertEquals($attempt->Email, 'sam@silverstripe.com'); $this->assertEquals($attempt->Email, 'sam@silverstripe.com');
$this->assertEquals($attempt->Member(), $member); $this->assertEquals($attempt->MemberID, $member->ID);
} }
public function testDatabaseIsReadyWithInsufficientMemberColumns() { public function testDatabaseIsReadyWithInsufficientMemberColumns() {