API Implement cascade_duplications

API Add DataObject::setComponent()
API Support unary components as getter and setter fields
API ManyManyList::add() now supports unsaved records
ENHANCEMENT Animal farm
This commit is contained in:
Damian Mooyman 2018-01-30 18:28:28 +13:00
parent b07babb9da
commit aa2c71424d
No known key found for this signature in database
GPG Key ID: 78B823A10DE27D1A
19 changed files with 661 additions and 327 deletions

View File

@ -420,6 +420,45 @@ If your object is versioned, cascade_deletes will also act as "cascade unpublish
on a parent object will trigger unpublish on the child, similarly to how `owns` causes triggered publishing. on a parent object will trigger unpublish on the child, similarly to how `owns` causes triggered publishing.
See the [versioning docs](/developer_guides/versioning) for more information on ownership. See the [versioning docs](/developer_guides/versioning) for more information on ownership.
## Cascading duplications
Similar to `cascade_deletes` there is also a `cascade_duplicates` config which works in much the same way.
When you invoke `$dataObject->duplicate()`, relation names specified by this config will be duplicated
and saved against the new clone object.
Note that duplications will act differently depending on the kind of relation:
- Exclusive relationships (e.g. has_many, belongs_to) will be explicitly duplicated.
- Non-exclusive many_many will not be duplicated, but the mapping table values will instead
be copied for this record.
- Non-exclusive has_one relationships are not normally necessary to duplicate, since both parent and clone
can normally share the same relation ID. However, if this is declared in `cascade_duplicates` any
has one will be similarly duplicated as though it were an exclusive relationship.
For example:
```php
use SilverStripe\ORM\DataObject;
class ParentObject extends DataObject {
private static $many_many = [
'RelatedChildren' => ChildObject::class,
];
private static $cascade_duplicates = [ 'RelatedChildren' ];
}
class ChildObject extends DataObject {
}
```
When duplicating objects you can disable recursive duplication by passing in `false` to the second
argument of duplicate().
E.g.
```php
$parent = ParentObject::get()->first();
$dupe = $parent->duplicate(true, false);
```
## Adding relations ## Adding relations
Adding new items to a relations works the same, regardless if you're editing a **has_many** or a **many_many**. They are Adding new items to a relations works the same, regardless if you're editing a **has_many** or a **many_many**. They are

View File

@ -139,7 +139,13 @@ trait FileUploadReceiver
$items = new ArrayList(); $items = new ArrayList();
// Determine format of presented data // Determine format of presented data
if (empty($value) && $record) { if ($value instanceof File) {
$items = ArrayList::create([$value]);
$value = null;
} elseif ($value instanceof SS_List) {
$items = $value;
$value = null;
} elseif (empty($value) && $record) {
// If a record is given as a second parameter, but no submitted values, // If a record is given as a second parameter, but no submitted values,
// then we should inspect this instead for the form values // then we should inspect this instead for the form values
@ -158,7 +164,7 @@ trait FileUploadReceiver
// If directly passing a list then save the items directly // If directly passing a list then save the items directly
$items = $record; $items = $record;
} }
} elseif (!empty($value['Files'])) { } elseif (is_array($value) && !empty($value['Files'])) {
// If value is given as an array (such as a posted form), extract File IDs from this // If value is given as an array (such as a posted form), extract File IDs from this
$class = $this->getRelationAutosetClass(); $class = $this->getRelationAutosetClass();
$items = DataObject::get($class)->byIDs($value['Files']); $items = DataObject::get($class)->byIDs($value['Files']);

View File

@ -1088,6 +1088,7 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li
* list manipulation * list manipulation
* *
* @param mixed $item * @param mixed $item
* @param array|null $extraFields Any extra fields, if supported by this list
*/ */
public function add($item) public function add($item)
{ {

View File

@ -259,7 +259,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* *
* @var DataObject[] * @var DataObject[]
*/ */
protected $components; protected $components = [];
/** /**
* Non-static cache of has_many and many_many relations that can't be written until this object is saved. * Non-static cache of has_many and many_many relations that can't be written until this object is saved.
@ -279,6 +279,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/ */
private static $cascade_deletes = []; private static $cascade_deletes = [];
/**
* List of relations that should be cascade duplicate.
* many_many duplications are shallow only.
*
* Note: If duplicating a many_many through you should refer to the
* has_many intermediary relation instead, otherwise extra fields
* will be omitted from the duplicated relation.
*
* @var array
*/
private static $cascade_duplicates = [];
/** /**
* Get schema object * Get schema object
* *
@ -292,7 +304,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/** /**
* Construct a new DataObject. * Construct a new DataObject.
* *
* @param array|null $record Used internally for rehydrating an object from database content. * @param array|null $record Used internally for rehydrating an object from database content.
* Bypasses setters on this class, and hence should not be used * Bypasses setters on this class, and hence should not be used
* for populating data on new records. * for populating data on new records.
@ -396,40 +407,105 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* *
* @param bool $doWrite Perform a write() operation before returning the object. * @param bool $doWrite Perform a write() operation before returning the object.
* If this is true, it will create the duplicate in the database. * If this is true, it will create the duplicate in the database.
* @param bool|string $manyMany Which many-many to duplicate. Set to true to duplicate all, false to duplicate none. * @param array|null|false $relations List of relations to duplicate.
* Alternatively set to the string of the relation config to duplicate * Will default to `cascade_duplicates` if null.
* (supports 'many_many', or 'belongs_many_many') * Set to 'false' to force none.
* Set to specific array of names to duplicate to override these.
* Note: If using versioned, this will additionally failover to `owns` config.
* @return static A duplicate of this node. The exact type will be the type of this node. * @return static A duplicate of this node. The exact type will be the type of this node.
*/ */
public function duplicate($doWrite = true, $manyMany = 'many_many') public function duplicate($doWrite = true, $relations = null)
{ {
// Handle legacy behaviour
if (is_string($relations) || $relations === true) {
if ($relations === true) {
$relations = 'many_many';
}
Deprecation::notice('5.0', 'Use cascade_duplicates config instead of providing a string to duplicate()');
$relations = array_keys($this->config()->get($relations)) ?: [];
}
// Get duplicates
if ($relations === null) {
$relations = $this->config()->get('cascade_duplicates');
}
// Create unsaved raw duplicate
$map = $this->toMap(); $map = $this->toMap();
unset($map['Created']); unset($map['Created']);
/** @var static $clone */ /** @var static $clone */
$clone = Injector::inst()->create(static::class, $map, false, $this->getSourceQueryParams()); $clone = Injector::inst()->create(static::class, $map, false, $this->getSourceQueryParams());
$clone->ID = 0; $clone->ID = 0;
$clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite, $manyMany); // Note: Extensions such as versioned may update $relations here
if ($manyMany) { $clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite, $relations);
$this->duplicateManyManyRelations($this, $clone, $manyMany); if ($relations) {
$this->duplicateRelations($this, $clone, $relations);
} }
if ($doWrite) { if ($doWrite) {
$clone->write(); $clone->write();
} }
$clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite, $manyMany); $clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite, $relations);
return $clone; return $clone;
} }
/**
* Copies the given relations from this object to the destination
*
* @param DataObject $sourceObject the source object to duplicate from
* @param DataObject $destinationObject the destination object to populate with the duplicated relations
* @param array $relations List of relations
*/
protected function duplicateRelations($sourceObject, $destinationObject, $relations)
{
// Get list of duplicable relation types
$manyMany = $sourceObject->manyMany();
$hasMany = $sourceObject->hasMany();
$hasOne = $sourceObject->hasOne();
$belongsTo = $sourceObject->belongsTo();
// Duplicate each relation based on type
foreach ($relations as $relation) {
switch (true) {
case array_key_exists($relation, $manyMany): {
$this->duplicateManyManyRelation($sourceObject, $destinationObject, $relation);
break;
}
case array_key_exists($relation, $hasMany): {
$this->duplicateHasManyRelation($sourceObject, $destinationObject, $relation);
break;
}
case array_key_exists($relation, $hasOne): {
$this->duplicateHasOneRelation($sourceObject, $destinationObject, $relation);
break;
}
case array_key_exists($relation, $belongsTo): {
$this->duplicateBelongsToRelation($sourceObject, $destinationObject, $relation);
break;
}
default: {
$sourceType = get_class($sourceObject);
throw new InvalidArgumentException(
"Cannot duplicate unknown relation {$relation} on parent type {$sourceType}"
);
}
}
}
}
/** /**
* Copies the many_many and belongs_many_many relations from one object to another instance of the name of object. * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object.
* *
* @deprecated 4.1...5.0 Use duplicateRelations() instead
* @param DataObject $sourceObject the source object to duplicate from * @param DataObject $sourceObject the source object to duplicate from
* @param DataObject $destinationObject the destination object to populate with the duplicated relations * @param DataObject $destinationObject the destination object to populate with the duplicated relations
* @param bool|string $filter * @param bool|string $filter
*/ */
protected function duplicateManyManyRelations($sourceObject, $destinationObject, $filter) protected function duplicateManyManyRelations($sourceObject, $destinationObject, $filter)
{ {
Deprecation::notice('5.0', 'Use duplicateRelations() instead');
// Get list of relations to duplicate // Get list of relations to duplicate
if ($filter === 'many_many' || $filter === 'belongs_many_many') { if ($filter === 'many_many' || $filter === 'belongs_many_many') {
$relations = $sourceObject->config()->get($filter); $relations = $sourceObject->config()->get($filter);
@ -444,25 +520,93 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
/** /**
* Duplicates a single many_many relation from one object to another * Duplicates a single many_many relation from one object to another.
* *
* @param DataObject $sourceObject * @param DataObject $sourceObject
* @param DataObject $destinationObject * @param DataObject $destinationObject
* @param string $manyManyName * @param string $relation
*/ */
protected function duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName) protected function duplicateManyManyRelation($sourceObject, $destinationObject, $relation)
{ {
// Ensure this component exists on the destination side as well // Copy all components from source to destination
if (!static::getSchema()->manyManyComponent(get_class($destinationObject), $manyManyName)) { $source = $sourceObject->getManyManyComponents($relation);
$dest = $destinationObject->getManyManyComponents($relation);
$extraFieldNames = $source->getExtraFields();
foreach ($source as $item) {
// Merge extra fields
$extraFields = [];
foreach ($extraFieldNames as $fieldName => $fieldType) {
$extraFields[$fieldName] = $item->getField($fieldName);
}
$dest->add($item, $extraFields);
}
}
/**
* Duplicates a single many_many relation from one object to another.
*
* @param DataObject $sourceObject
* @param DataObject $destinationObject
* @param string $relation
*/
protected function duplicateHasManyRelation($sourceObject, $destinationObject, $relation)
{
// Copy all components from source to destination
$source = $sourceObject->getComponents($relation);
$dest = $destinationObject->getComponents($relation);
/** @var DataObject $item */
foreach ($source as $item) {
// Don't write on duplicate; Wait until ParentID is available later.
// writeRelations() will eventually write these records when converting
// from UnsavedRelationList
$clonedItem = $item->duplicate(false);
$dest->add($clonedItem);
}
}
/**
* Duplicates a single has_one relation from one object to another.
* Note: Child object will be force written.
*
* @param DataObject $sourceObject
* @param DataObject $destinationObject
* @param string $relation
*/
protected function duplicateHasOneRelation($sourceObject, $destinationObject, $relation)
{
// Check if original object exists
$item = $sourceObject->getComponent($relation);
if (!$item->isInDB()) {
return; return;
} }
// Copy all components from source to destination $clonedItem = $item->duplicate(false);
$source = $sourceObject->getManyManyComponents($manyManyName); $destinationObject->setComponent($relation, $clonedItem);
$dest = $destinationObject->getManyManyComponents($manyManyName);
foreach ($source as $item) {
$dest->add($item);
} }
/**
* Duplicates a single belongs_to relation from one object to another.
* Note: This will force a write on both parent / child objects.
*
* @param DataObject $sourceObject
* @param DataObject $destinationObject
* @param string $relation
*/
protected function duplicateBelongsToRelation($sourceObject, $destinationObject, $relation)
{
// Check if original object exists
$item = $sourceObject->getComponent($relation);
if (!$item->isInDB()) {
return;
}
$clonedItem = $item->duplicate(false);
$destinationObject->setComponent($relation, $clonedItem);
// After $clonedItem is assigned the appropriate FieldID / FieldClass, force write
// @todo Write this component in onAfterWrite instead, assigning the FieldID then
// https://github.com/silverstripe/silverstripe-framework/issues/7818
$clonedItem->write();
} }
/** /**
@ -1232,7 +1376,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
// Ensure this field pertains to this table // Ensure this field pertains to this table
$specification = $schema->fieldSpec($class, $fieldName, DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED); $specification = $schema->fieldSpec(
$class,
$fieldName,
DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED
);
if (!$specification) { if (!$specification) {
continue; continue;
} }
@ -1251,8 +1399,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if ($baseTable === $table) { if ($baseTable === $table) {
$manipulation[$table]['fields']['LastEdited'] = $now; $manipulation[$table]['fields']['LastEdited'] = $now;
if ($isNewRecord) { if ($isNewRecord) {
$manipulation[$table]['fields']['Created'] $manipulation[$table]['fields']['Created'] = empty($this->record['Created'])
= empty($this->record['Created'])
? $now ? $now
: $this->record['Created']; : $this->record['Created'];
$manipulation[$table]['fields']['ClassName'] = static::class; $manipulation[$table]['fields']['ClassName'] = static::class;
@ -1414,11 +1561,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/ */
public function writeComponents($recursive = false) public function writeComponents($recursive = false)
{ {
if ($this->components) {
foreach ($this->components as $component) { foreach ($this->components as $component) {
$component->write(false, false, false, $recursive); $component->write(false, false, false, $recursive);
} }
}
if ($join = $this->getJoin()) { if ($join = $this->getJoin()) {
$join->write(false, false, false, $recursive); $join->write(false, false, false, $recursive);
@ -1501,7 +1646,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
/** /**
* Return a component object from a one to one relationship, as a DataObject. * Return a unary component object from a one to one relationship, as a DataObject.
* If no component is available, an 'empty component' will be returned for * If no component is available, an 'empty component' will be returned for
* non-polymorphic relations, or for polymorphic relations with a class set. * non-polymorphic relations, or for polymorphic relations with a class set.
* *
@ -1582,6 +1727,52 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $component; return $component;
} }
/**
* Assign an item to the given component
*
* @param string $componentName
* @param DataObject|null $item
* @return $this
*/
public function setComponent($componentName, $item)
{
// Validate component
$schema = static::getSchema();
if ($class = $schema->hasOneComponent(static::class, $componentName)) {
// Force item to be written if not by this point
// @todo This could be lazy-written in a beforeWrite hook, but force write here for simplicity
// https://github.com/silverstripe/silverstripe-framework/issues/7818
if ($item && !$item->isInDB()) {
$item->write();
}
// Update local ID
$joinField = $componentName . 'ID';
$this->setField($joinField, $item ? $item->ID : null);
// Update Class (Polymorphic has_one)
// Extract class name for polymorphic relations
if ($class === self::class) {
$this->setField($componentName . 'Class', $item ? get_class($item) : null);
}
} elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
if ($item) {
// For belongs_to, add to has_one on other component
$joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
if (!$polymorphic) {
$joinField = substr($joinField, 0, -2);
}
$item->setComponent($joinField, $this);
}
} else {
throw new InvalidArgumentException(
"DataObject->setComponent(): Could not find component '$componentName'."
);
}
$this->components[$componentName] = $item;
return $this;
}
/** /**
* Returns a one-to-many relation as a HasManyList * Returns a one-to-many relation as a HasManyList
* *
@ -1752,7 +1943,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
case 'belongs_to': case 'belongs_to':
case 'has_many': { case 'has_many': {
// These relations must have a has_one on the other end, so find it // These relations must have a has_one on the other end, so find it
$joinField = $schema->getRemoteJoinField($remoteClass, $remoteRelation, $relationType, $polymorphic); $joinField = $schema->getRemoteJoinField(
$remoteClass,
$remoteRelation,
$relationType,
$polymorphic
);
if ($polymorphic) { if ($polymorphic) {
throw new InvalidArgumentException(sprintf( throw new InvalidArgumentException(sprintf(
"%s cannot generate opposite component of relation %s.%s, as the other end appears" . "%s cannot generate opposite component of relation %s.%s, as the other end appears" .
@ -1806,7 +2002,7 @@ 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
* @param int|array $id Optional ID for parent of this relation, if not the current record * @param int|array $id Optional ID for parent of this relation, if not the current record
* @return RelationList|UnsavedRelationList The set of components * @return ManyManyList|UnsavedRelationList The set of components
*/ */
public function getManyManyComponents($componentName, $id = null) public function getManyManyComponents($componentName, $id = null)
{ {
@ -1827,7 +2023,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
if (!$id) { if (!$id) {
if (!isset($this->unsavedRelations[$componentName])) { if (!isset($this->unsavedRelations[$componentName])) {
$this->unsavedRelations[$componentName] = $this->unsavedRelations[$componentName] =
new UnsavedRelationList($manyManyComponent['parentClass'], $componentName, $manyManyComponent['childClass']); new UnsavedRelationList(
$manyManyComponent['parentClass'],
$componentName,
$manyManyComponent['childClass']
);
} }
return $this->unsavedRelations[$componentName]; return $this->unsavedRelations[$componentName];
} }
@ -2180,9 +2380,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$tableClass = $this->record[$field . '_Lazy']; $tableClass = $this->record[$field . '_Lazy'];
$this->loadLazyFields($tableClass); $this->loadLazyFields($tableClass);
} }
$schema = static::getSchema();
// Support unary relations as fields
if ($schema->unaryComponent(static::class, $field)) {
return $this->getComponent($field);
}
// In case of complex fields, return the DBField object // In case of complex fields, return the DBField object
if (static::getSchema()->compositeField(static::class, $field)) { if ($schema->compositeField(static::class, $field)) {
$this->record[$field] = $this->dbObject($field); $this->record[$field] = $this->dbObject($field);
} }
@ -2382,6 +2588,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->loadLazyFields($tableClass); $this->loadLazyFields($tableClass);
} }
// Support component assignent via field setter
$schema = static::getSchema();
if ($schema->unaryComponent(static::class, $fieldName)) {
// Assign component directly
if (is_null($val) || $val instanceof DataObject) {
return $this->setComponent($fieldName, $val);
}
// Assign by ID instead of object
unset($this->components[$fieldName]);
$fieldName .= 'ID';
}
// Situation 1: Passing an DBField // Situation 1: Passing an DBField
if ($val instanceof DBField) { if ($val instanceof DBField) {
$val->setName($fieldName); $val->setName($fieldName);
@ -2396,7 +2614,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Situation 2: Passing a literal or non-DBField object // Situation 2: Passing a literal or non-DBField object
} else { } else {
// If this is a proper database field, we shouldn't be getting non-DBField objects // If this is a proper database field, we shouldn't be getting non-DBField objects
if (is_object($val) && static::getSchema()->fieldSpec(static::class, $fieldName)) { if (is_object($val) && $schema->fieldSpec(static::class, $fieldName)) {
throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField'); throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
} }
@ -2483,8 +2701,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$schema = static::getSchema(); $schema = static::getSchema();
return ( return (
array_key_exists($field, $this->record) array_key_exists($field, $this->record)
|| array_key_exists($field, $this->components)
|| $schema->fieldSpec(static::class, $field) || $schema->fieldSpec(static::class, $field)
|| (substr($field, -2) == 'ID') && $schema->hasOneComponent(static::class, substr($field, 0, -2)) || $schema->unaryComponent(static::class, $field)
|| $this->hasMethod("get{$field}") || $this->hasMethod("get{$field}")
); );
} }
@ -3288,13 +3507,28 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
]; ];
if ($includerelations) { if ($includerelations) {
$types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED); $types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
$types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED); $types['has_many'] = (array)Config::inst()->get(
$types['many_many'] = (array)Config::inst()->get($ancestorClass, 'many_many', Config::UNINHERITED); $ancestorClass,
$types['belongs_many_many'] = (array)Config::inst()->get($ancestorClass, 'belongs_many_many', Config::UNINHERITED); 'has_many',
Config::UNINHERITED
);
$types['many_many'] = (array)Config::inst()->get(
$ancestorClass,
'many_many',
Config::UNINHERITED
);
$types['belongs_many_many'] = (array)Config::inst()->get(
$ancestorClass,
'belongs_many_many',
Config::UNINHERITED
);
} }
foreach ($types as $type => $attrs) { foreach ($types as $type => $attrs) {
foreach ($attrs as $name => $spec) { foreach ($attrs as $name => $spec) {
$autoLabels[$name] = _t("{$ancestorClass}.{$type}_{$name}", FormField::name_to_label($name)); $autoLabels[$name] = _t(
"{$ancestorClass}.{$type}_{$name}",
FormField::name_to_label($name)
);
} }
} }
} }
@ -3428,6 +3662,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
{ {
self::$subclass_access = false; self::$subclass_access = false;
} }
public static function enable_subclass_access() public static function enable_subclass_access()
{ {
self::$subclass_access = true; self::$subclass_access = true;

View File

@ -908,6 +908,19 @@ class DataObjectSchema
return $classOnly ? $belongsToClass : $belongsTo; return $classOnly ? $belongsToClass : $belongsTo;
} }
/**
* Check class for any unary component
*
* Alias for hasOneComponent() ?: belongsToComponent()
* @param string $class
* @param string $component
* @return string|null
*/
public function unaryComponent($class, $component)
{
return $this->hasOneComponent($class, $component) ?: $this->belongsToComponent($class, $component);
}
/** /**
* *
* @param string $parentClass Parent class name * @param string $parentClass Parent class name

View File

@ -225,6 +225,10 @@ class ManyManyList extends RelationList
if (is_numeric($item)) { if (is_numeric($item)) {
$itemID = $item; $itemID = $item;
} elseif ($item instanceof $this->dataClass) { } elseif ($item instanceof $this->dataClass) {
// Ensure record is saved
if (!$item->isInDB()) {
$item->write();
}
$itemID = $item->ID; $itemID = $item->ID;
} else { } else {
throw new InvalidArgumentException( throw new InvalidArgumentException(
@ -232,7 +236,7 @@ class ManyManyList extends RelationList
); );
} }
if (empty($itemID)) { if (empty($itemID)) {
throw new InvalidArgumentException("ManyManyList::add() doesn't accept unsaved records"); throw new InvalidArgumentException("ManyManyList::add() couldn't add this record");
} }
// Validate foreignID // Validate foreignID

View File

@ -82,10 +82,6 @@ class UnsavedRelationList extends ArrayList implements Relation
public function changeToList(RelationList $list) public function changeToList(RelationList $list)
{ {
foreach ($this->items as $key => $item) { foreach ($this->items as $key => $item) {
if (is_object($item)) {
/** @var DataObject $item */
$item->write();
}
$list->add($item, $this->extraFields[$key]); $list->add($item, $this->extraFields[$key]);
} }
} }

View File

@ -2,32 +2,30 @@
namespace SilverStripe\ORM\Tests; namespace SilverStripe\ORM\Tests;
use SilverStripe\ORM\DataObject;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\FieldType\DBDatetime;
class DataObjectDuplicationTest extends SapphireTest class DataObjectDuplicationTest extends SapphireTest
{ {
protected static $fixture_file = 'DataObjectDuplicationTest.yml';
protected $usesDatabase = true; protected static $extra_dataobjects = [
DataObjectDuplicationTest\Antelope::class,
protected static $extra_dataobjects = array( DataObjectDuplicationTest\Bobcat::class,
DataObjectDuplicationTest\Class1::class, DataObjectDuplicationTest\Caribou::class,
DataObjectDuplicationTest\Class2::class, DataObjectDuplicationTest\Dingo::class,
DataObjectDuplicationTest\Class3::class, DataObjectDuplicationTest\Elephant::class,
DataObjectDuplicationTest\Class4::class, DataObjectDuplicationTest\Frog::class,
); DataObjectDuplicationTest\Goat::class,
];
public function testDuplicate() public function testDuplicate()
{ {
DBDatetime::set_mock_now('2016-01-01 01:01:01'); /** @var DataObjectDuplicationTest\Antelope $orig */
$orig = new DataObjectDuplicationTest\Class1(); $orig = $this->objFromFixture(DataObjectDuplicationTest\Antelope::class, 'one');
$orig->text = 'foo'; /** @var DataObjectDuplicationTest\Antelope $duplicate */
$orig->write();
DBDatetime::set_mock_now('2016-01-02 01:01:01');
$duplicate = $orig->duplicate(); $duplicate = $orig->duplicate();
$this->assertInstanceOf( $this->assertInstanceOf(
DataObjectDuplicationTest\Class1::class, DataObjectDuplicationTest\Antelope::class,
$duplicate, $duplicate,
'Creates the correct type' 'Creates the correct type'
); );
@ -36,198 +34,79 @@ class DataObjectDuplicationTest extends SapphireTest
$orig->ID, $orig->ID,
'Creates a unique record' 'Creates a unique record'
); );
$this->assertEquals(
'foo',
$duplicate->text,
'Copies fields'
);
$this->assertEquals(
2,
DataObjectDuplicationTest\Class1::get()->Count(),
'Only creates a single duplicate'
);
$this->assertEquals(DBDatetime::now()->Nice(), $duplicate->dbObject('Created')->Nice());
$this->assertNotEquals($orig->dbObject('Created')->Nice(), $duplicate->dbObject('Created')->Nice());
}
public function testDuplicateHasOne() // Check 'bobcats' relation duplicated
{ $twoOne = $this->objFromFixture(DataObjectDuplicationTest\Bobcat::class, 'one');
$relationObj = new DataObjectDuplicationTest\Class1(); $twoTwo = $this->objFromFixture(DataObjectDuplicationTest\Bobcat::class, 'two');
$relationObj->text = 'class1';
$relationObj->write();
$orig = new DataObjectDuplicationTest\Class2();
$orig->text = 'class2';
$orig->oneID = $relationObj->ID;
$orig->write();
$duplicate = $orig->duplicate();
$this->assertEquals(
$relationObj->ID,
$duplicate->oneID,
'Copies has_one relationship'
);
$this->assertEquals(
2,
DataObjectDuplicationTest\Class2::get()->Count(),
'Only creates a single duplicate'
);
$this->assertEquals(
1,
DataObjectDuplicationTest\Class1::get()->Count(),
'Does not create duplicate of has_one relationship'
);
}
public function testDuplicateManyManyClasses()
{
//create new test classes below
$one = new DataObjectDuplicationTest\Class1();
$two = new DataObjectDuplicationTest\Class2();
$three = new DataObjectDuplicationTest\Class3();
//set some simple fields
$text1 = "Test Text 1";
$text2 = "Test Text 2";
$text3 = "Test Text 3";
$one->text = $text1;
$two->text = $text2;
$three->text = $text3;
//write the to DB
$one->write();
$two->write();
$three->write();
//create relations
$one->twos()->add($two);
$one->threes()->add($three);
$one = DataObject::get_by_id(DataObjectDuplicationTest\Class1::class, $one->ID);
$two = DataObject::get_by_id(DataObjectDuplicationTest\Class2::class, $two->ID);
$three = DataObject::get_by_id(DataObjectDuplicationTest\Class3::class, $three->ID);
//test duplication
$oneCopy = $one->duplicate(true, true);
$twoCopy = $two->duplicate(true, true);
$threeCopy = $three->duplicate(true, true);
$oneCopy = DataObject::get_by_id(DataObjectDuplicationTest\Class1::class, $oneCopy->ID);
$twoCopy = DataObject::get_by_id(DataObjectDuplicationTest\Class2::class, $twoCopy->ID);
$threeCopy = DataObject::get_by_id(DataObjectDuplicationTest\Class3::class, $threeCopy->ID);
$this->assertNotNull($oneCopy, "Copy of 1 exists");
$this->assertNotNull($twoCopy, "Copy of 2 exists");
$this->assertNotNull($threeCopy, "Copy of 3 exists");
$this->assertEquals($text1, $oneCopy->text);
$this->assertEquals($text2, $twoCopy->text);
$this->assertEquals($text3, $threeCopy->text);
$this->assertNotEquals(
$one->twos()->Count(),
$oneCopy->twos()->Count(),
"Many-to-one relation not copied (has_many)"
);
$this->assertEquals(
$one->threes()->Count(),
$oneCopy->threes()->Count(),
"Object has the correct number of relations"
);
$this->assertEquals(
$three->ones()->Count(),
$threeCopy->ones()->Count(),
"Object has the correct number of relations"
);
$this->assertEquals(
$one->ID,
$twoCopy->one()->ID,
"Match between relation of copy and the original"
);
$this->assertEquals(
0,
$oneCopy->twos()->Count(),
"Many-to-one relation not copied (has_many)"
);
$this->assertEquals(
$three->ID,
$oneCopy->threes()->First()->ID,
"Match between relation of copy and the original"
);
$this->assertEquals(
$one->ID,
$threeCopy->ones()->First()->ID,
"Match between relation of copy and the original"
);
}
public function testDuplicateManyManyFiltered()
{
$parent = new DataObjectDuplicationTest\Class4();
$parent->Title = 'Parent';
$parent->write();
$child = new DataObjectDuplicationTest\Class4();
$child->Title = 'Child';
$child->write();
$grandChild = new DataObjectDuplicationTest\Class4();
$grandChild->Title = 'GrandChild';
$grandChild->write();
$parent->Children()->add($child);
$child->Children()->add($grandChild);
// Duplcating $child should only duplicate grandchild
$childDuplicate = $child->duplicate(true, 'many_many');
$this->assertEquals(0, $childDuplicate->Parents()->count());
$this->assertListEquals( $this->assertListEquals(
[['Title' => 'GrandChild']], [
$childDuplicate->Children() ['Title' => 'Bobcat two'],
['Title' => 'Bobcat three'],
],
$duplicate->bobcats()
); );
$this->assertEmpty(
// Duplicate belongs_many_many only array_intersect(
$belongsDuplicate = $child->duplicate(true, 'belongs_many_many'); $orig->bobcats()->getIDList(),
$this->assertEquals(0, $belongsDuplicate->Children()->count()); $duplicate->bobcats()->getIDList()
$this->assertListEquals( )
[['Title' => 'Parent']],
$belongsDuplicate->Parents()
); );
/** @var DataObjectDuplicationTest\Bobcat $twoTwoDuplicate */
$twoTwoDuplicate = $duplicate->bobcats()->filter('Title', 'Bobcat two')->first();
$this->assertNotEmpty($twoTwoDuplicate);
$this->assertNotEquals($twoTwo->ID, $twoTwoDuplicate->ID);
// Duplicate all // Check 'bobcats.self' relation duplicated
$allDuplicate = $child->duplicate(true, true); /** @var DataObjectDuplicationTest\Bobcat $twoOneDuplicate */
$twoOneDuplicate = $twoTwoDuplicate->self();
$this->assertNotEmpty($twoOneDuplicate);
$this->assertNotEquals($twoOne->ID, $twoOneDuplicate->ID);
// Ensure 'bobcats.seven' instance is not duplicated
$sevenOne = $this->objFromFixture(DataObjectDuplicationTest\Goat::class, 'one');
$sevenTwo = $this->objFromFixture(DataObjectDuplicationTest\Goat::class, 'two');
$this->assertEquals($sevenOne->ID, $twoOneDuplicate->goat()->ID);
$this->assertEquals($sevenTwo->ID, $twoTwoDuplicate->goat()->ID);
// Ensure that 'caribou' many_many list exists on both, but only the mapping table is duplicated
// many_many_extraFields are also duplicated
$caribouList = [
[
'Title' => 'Caribou one',
'Sort' => 4,
],
[
'Title' => 'Caribou two',
'Sort' => 5,
],
];
// Original and duplicate lists have the same content
$this->assertListEquals( $this->assertListEquals(
[['Title' => 'Parent']], $caribouList,
$allDuplicate->Parents() $orig->caribou()
); );
$this->assertListEquals( $this->assertListEquals(
[['Title' => 'GrandChild']], $caribouList,
$allDuplicate->Children() $duplicate->caribou()
);
// Ids of each record are the same (only mapping content is duplicated)
$this->assertEquals(
$orig->caribou()->getIDList(),
$duplicate->caribou()->getIDList()
); );
}
/** // Ensure 'five' belongs_to is duplicated
* Test duplication of UnsavedRelations $fiveOne = $this->objFromFixture(DataObjectDuplicationTest\Elephant::class, 'one');
*/ $fiveOneDuplicate = $duplicate->elephant();
public function testDuplicateUnsaved() $this->assertNotEmpty($fiveOneDuplicate);
{ $this->assertEquals('Elephant one', $fiveOneDuplicate->Title);
$one = new DataObjectDuplicationTest\Class1(); $this->assertNotEquals($fiveOne->ID, $fiveOneDuplicate->ID);
$one->text = "Test Text 1";
$three = new DataObjectDuplicationTest\Class3(); // Ensure 'five.Child' is duplicated
$three->text = "Test Text 3"; $sixOne = $this->objFromFixture(DataObjectDuplicationTest\Frog::class, 'one');
$one->threes()->add($three); $sixOneDuplicate = $fiveOneDuplicate->Child();
$this->assertListEquals( $this->assertNotEmpty($sixOneDuplicate);
[['text' => 'Test Text 3']], $this->assertEquals('Frog one', $sixOneDuplicate->Title);
$one->threes() $this->assertNotEquals($sixOne->ID, $sixOneDuplicate->ID);
);
// Test duplicate
$dupe = $one->duplicate(false, true);
$this->assertEquals('Test Text 1', $dupe->text);
$this->assertListEquals(
[['text' => 'Test Text 3']],
$dupe->threes()
);
} }
} }

View File

@ -0,0 +1,42 @@
SilverStripe\ORM\Tests\DataObjectDuplicationTest\Antelope:
one:
Title: 'Antelope one'
SilverStripe\ORM\Tests\DataObjectDuplicationTest\Goat:
one:
Title: 'Goat one'
two:
Title: 'Goat two'
SilverStripe\ORM\Tests\DataObjectDuplicationTest\Bobcat:
one:
Title: 'Bobcat one'
goat: =>SilverStripe\ORM\Tests\DataObjectDuplicationTest\Goat.one
two:
Title: 'Bobcat two'
antelope: =>SilverStripe\ORM\Tests\DataObjectDuplicationTest\Antelope.one
self: =>SilverStripe\ORM\Tests\DataObjectDuplicationTest\Bobcat.one
goat: =>SilverStripe\ORM\Tests\DataObjectDuplicationTest\Goat.two
three:
Title: 'Bobcat three'
antelope: =>SilverStripe\ORM\Tests\DataObjectDuplicationTest\Antelope.one
SilverStripe\ORM\Tests\DataObjectDuplicationTest\Caribou:
one:
Title: 'Caribou one'
two:
Title: 'Caribou two'
DataObjectDuplicateTest_Antelope_caribou:
one:
DataObjectDuplicateTest_AntelopeID: =>SilverStripe\ORM\Tests\DataObjectDuplicationTest\Antelope.one
DataObjectDuplicateTest_CaribouID: =>SilverStripe\ORM\Tests\DataObjectDuplicationTest\Caribou.one
Sort: 4
two:
DataObjectDuplicateTest_AntelopeID: =>SilverStripe\ORM\Tests\DataObjectDuplicationTest\Antelope.one
DataObjectDuplicateTest_CaribouID: =>SilverStripe\ORM\Tests\DataObjectDuplicationTest\Caribou.two
Sort: 5
SilverStripe\ORM\Tests\DataObjectDuplicationTest\Frog:
one:
Title: 'Frog one'
SilverStripe\ORM\Tests\DataObjectDuplicationTest\Elephant:
one:
Title: 'Elephant one'
Parent: =>SilverStripe\ORM\Tests\DataObjectDuplicationTest\Antelope.one
Child: =>SilverStripe\ORM\Tests\DataObjectDuplicationTest\Frog.one

View File

@ -0,0 +1,46 @@
<?php
namespace SilverStripe\ORM\Tests\DataObjectDuplicationTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\ManyManyList;
/**
* @method HasManyList bobcats()
* @method ManyManyList caribou()
* @method Elephant elephant()
*/
class Antelope extends DataObject implements TestOnly
{
private static $table_name = 'DataObjectDuplicateTest_Antelope';
private static $cascade_duplicates = [
'bobcats',
'caribou',
'elephant',
];
private static $db = [
'Title' => 'Varchar',
];
private static $has_many = [
'bobcats' => Bobcat::class,
];
private static $many_many = [
'caribou' => Caribou::class,
];
private static $many_many_extraFields = [
'caribou' => [
'Sort' => 'Int',
],
];
private static $belongs_to = [
'elephant' => Elephant::class,
];
}

View File

@ -0,0 +1,30 @@
<?php
namespace SilverStripe\ORM\Tests\DataObjectDuplicationTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
/**
* @method Antelope antelope()
* @method Bobcat self()
* @method Goat goat()
*/
class Bobcat extends DataObject implements TestOnly
{
private static $table_name = 'DataObjectDuplicateTest_Bobcat';
private static $cascade_duplicates = [
'self',
];
private static $db = [
'Title' => 'Varchar',
];
private static $has_one = [
'antelope' => Antelope::class,
'self' => Bobcat::class,
'goat' => Goat::class,
];
}

View File

@ -0,0 +1,23 @@
<?php
namespace SilverStripe\ORM\Tests\DataObjectDuplicationTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ManyManyList;
/**
* @method ManyManyList antelopes()
*/
class Caribou extends DataObject implements TestOnly
{
private static $table_name = 'DataObjectDuplicateTest_Caribou';
private static $db = [
'Title' => 'Varchar',
];
private static $belongs_many_many = [
'antelopes' => Antelope::class,
];
}

View File

@ -1,23 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\DataObjectDuplicationTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
class Class1 extends DataObject implements TestOnly
{
private static $table_name = 'DataObjectDuplicateTest_Class1';
private static $db = array(
'text' => 'Varchar'
);
private static $has_many = array(
'twos' => Class2::class
);
private static $many_many = array(
'threes' => Class3::class
);
}

View File

@ -1,19 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\DataObjectDuplicationTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
class Class2 extends DataObject implements TestOnly
{
private static $table_name = 'DataObjectDuplicateTest_Class2';
private static $db = array(
'text' => 'Varchar'
);
private static $has_one = array(
'one' => Class1::class
);
}

View File

@ -1,19 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\DataObjectDuplicationTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
class Class3 extends DataObject implements TestOnly
{
private static $table_name = 'DataObjectDuplicateTest_Class3';
private static $db = array(
'text' => 'Varchar'
);
private static $belongs_many_many = array(
'ones' => Class1::class
);
}

View File

@ -11,19 +11,23 @@ use SilverStripe\ORM\ManyManyList;
* @method ManyManyList Children() * @method ManyManyList Children()
* @method ManyManyList Parents() * @method ManyManyList Parents()
*/ */
class Class4 extends DataObject implements TestOnly class Dingo extends DataObject implements TestOnly
{ {
private static $table_name = 'DataObjectDuplicateTest_Class4'; private static $table_name = 'DataObjectDuplicateTest_Dingo';
private static $cascade_duplicates = [
'Children',
];
private static $db = [ private static $db = [
'Title' => 'Varchar', 'Title' => 'Varchar',
]; ];
private static $many_many = [ private static $many_many = [
'Children' => Class4::class, 'Children' => Dingo::class,
]; ];
private static $belongs_many_many = [ private static $belongs_many_many = [
'Parents' => Class4::class, 'Parents' => Dingo::class,
]; ];
} }

View File

@ -0,0 +1,29 @@
<?php
namespace SilverStripe\ORM\Tests\DataObjectDuplicationTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
/**
* @method Antelope Parent()
* @method Frog Child()
*/
class Elephant extends DataObject implements TestOnly
{
private static $table_name = 'DataObjectDuplicateTest_Elephant';
private static $cascade_duplicates = [
'Child',
];
private static $db = [
'Title' => 'Varchar',
];
private static $has_one = [
'Parent' => Antelope::class,
'Child' => Frog::class,
];
}

View File

@ -0,0 +1,25 @@
<?php
namespace SilverStripe\ORM\Tests\DataObjectDuplicationTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ManyManyList;
/**
* @method ManyManyList Children()
* @method ManyManyList Parents()
*/
class Frog extends DataObject implements TestOnly
{
private static $table_name = 'DataObjectDuplicateTest_Frog';
private static $db = [
'Title' => 'Varchar',
];
private static $belongs_to = [
'Parent' => Elephant::class,
];
}

View File

@ -0,0 +1,23 @@
<?php
namespace SilverStripe\ORM\Tests\DataObjectDuplicationTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
/**
* Note: Not duplicated
*/
class Goat extends DataObject implements TestOnly
{
private static $table_name = 'DataObjectDuplicateTest_Goat';
private static $db = [
'Title' => 'Varchar',
];
private static $belongs_to = [
'bobcats' => Bobcat::class,
];
}