Merge pull request #5219 from open-sausages/pulls/4.0/optional-owned-by

API Improvements to ownership api
This commit is contained in:
Hamish Friedlander 2016-03-24 10:08:40 +13:00
commit 1e53f29f33
8 changed files with 649 additions and 64 deletions

View File

@ -12,6 +12,7 @@
* @subpackage core
*/
abstract class Extension {
/**
* This is used by extensions designed to be applied to controllers.
* It works the same way as {@link Controller::$allowed_actions}.
@ -32,10 +33,12 @@ abstract class Extension {
protected $ownerBaseClass;
/**
* Reference counter to ensure that the owner isn't cleared until clearOwner() has
* been called as many times as setOwner()
* Ownership stack for recursive methods.
* Last item is current owner.
*
* @var array
*/
private $ownerRefs = 0;
private $ownerStack = [];
public $class;
@ -55,6 +58,7 @@ abstract class Extension {
/**
* Set the owner of this extension.
*
* @param Object $owner The owner object,
* @param string $ownerBaseClass The base class that the extension is applied to; this may be
* the class of owner, or it may be a parent. For example, if Versioned was applied to SiteTree,
@ -62,17 +66,32 @@ abstract class Extension {
* would be 'SiteTree'.
*/
public function setOwner($owner, $ownerBaseClass = null) {
if($owner) $this->ownerRefs++;
if($owner) {
$this->ownerStack[] = $owner;
}
$this->owner = $owner;
if($ownerBaseClass) $this->ownerBaseClass = $ownerBaseClass;
else if(!$this->ownerBaseClass && $owner) $this->ownerBaseClass = $owner->class;
// Set ownerBaseClass
if($ownerBaseClass) {
$this->ownerBaseClass = $ownerBaseClass;
} elseif(!$this->ownerBaseClass && $owner) {
$this->ownerBaseClass = get_class($owner);
}
}
/**
* Clear the current owner, and restore extension to the state prior to the last setOwner()
*/
public function clearOwner() {
if($this->ownerRefs <= 0) user_error("clearOwner() called more than setOwner()", E_USER_WARNING);
$this->ownerRefs--;
if($this->ownerRefs == 0) $this->owner = null;
if(empty($this->ownerStack)) {
throw new BadMethodCallException("clearOwner() called more than setOwner()");
}
array_pop($this->ownerStack);
if($this->ownerStack) {
$this->owner = end($this->ownerStack);
} else {
$this->owner = null;
}
}
/**
@ -97,7 +116,4 @@ abstract class Extension {
return $parts[0];
}
}

View File

@ -148,7 +148,7 @@ is initialized. But it can also be set and reset temporarily to force a specific
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records
Versioned::set_reading_mode($origMode); // reset current mode
### File ownership
### DataObject 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.
@ -160,8 +160,8 @@ The solution to this problem is the ownership API, which declares a two-way rela
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.
For instance, in order to specify this dependency, you must apply `owns` on the owner to point to any
owned relationships.
When pages of type `MyPage` are published, any owned images and banners will be automatically published,
without requiring any custom code.
@ -185,33 +185,52 @@ without requiring any custom code.
'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'
Note that ownership cannot be used with polymorphic relations. E.g. has_one to non-type specific `DataObject`.
#### DataObject ownership with custom relations
In some cases you might need to apply ownership where there is no underlying db relation, such as
those calculated at runtime based on business logic. In cases where you are not backing ownership
with standard relations (has_one, has_many, etc) it is necessary to declare ownership on both
sides of the relation.
This can be done by creating methods on both sides of your relation (e.g. parent and child class)
that can be used to traverse between each, and then by ensuring you configure both
`owns` config (on the parent) and `owned_by` (on the child).
E.g.
:::php
class MyParent extends DataObject {
private static $extensions = array(
'Versioned'
);
private static $owns = array(
'ChildObjects'
);
public function ChildObjects() {
return MyChild::get();
}
}
class MyChild extends DataObject {
private static $extensions = array(
'Versioned'
);
private static $owned_by = array(
'Banners'
'Parent'
);
public function Parent() {
return MyParent::get()->first();
}
}
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`.
#### DataObject Ownership in HTML Content
If you are using `[api:HTMLText]` or `[api:HTMLVarchar]` fields in your `DataObject::$db` definitions,
it's likely that your authors can insert images into those fields via the CMS interface.

View File

@ -1626,7 +1626,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/**
* Find the foreign class of a relation on this DataObject, regardless of the relation type.
*
* @param $relationName Relation name.
* @param string $relationName Relation name.
* @return string Class name, or null if not found.
*/
public function getRelationClass($relationName) {
@ -1654,6 +1654,137 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return null;
}
/**
* Given a relation name, determine the relation type
*
* @param string $component Name of component
* @return string has_one, has_many, many_many, belongs_many_many or belongs_to
*/
public function getRelationType($component) {
$types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
foreach($types as $type) {
$relations = Config::inst()->get($this->class, $type);
if($relations && isset($relations[$component])) {
return $type;
}
}
return null;
}
/**
* Given a relation declared on a remote class, generate a substitute component for the opposite
* side of the relation.
*
* Notes on behaviour:
* - This can still be used on components that are defined on both sides, but do not need to be.
* - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
* - Cannot be used on polymorphic relationships
* - Cannot be used on unsaved objects.
*
* @param string $remoteClass
* @param string $remoteRelation
* @return DataList|DataObject The component, either as a list or single object
* @throws BadMethodCallException
* @throws InvalidArgumentException
*/
public function inferReciprocalComponent($remoteClass, $remoteRelation) {
/** @var DataObject $remote */
$remote = $remoteClass::singleton();
$class = $remote->getRelationClass($remoteRelation);
// Validate arguments
if(!$this->isInDB()) {
throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
}
if(empty($class)) {
throw new InvalidArgumentException(sprintf(
"%s invoked with invalid relation %s.%s",
__METHOD__,
$remoteClass,
$remoteRelation
));
}
if($class === 'DataObject') {
throw new InvalidArgumentException(sprintf(
"%s cannot generate opposite component of relation %s.%s as it is polymorphic. " .
"This method does not support polymorphic relationships",
__METHOD__,
$remoteClass,
$remoteRelation
));
}
if(!is_a($this, $class, true)) {
throw new InvalidArgumentException(sprintf(
"Relation %s on %s does not refer to objects of type %s",
$remoteRelation, $remoteClass, get_class($this)
));
}
// Check the relation type to mock
$relationType = $remote->getRelationType($remoteRelation);
switch($relationType) {
case 'has_one': {
// Mock has_many
$joinField = "{$remoteRelation}ID";
$componentClass = ClassInfo::table_for_object_field($remoteClass, $joinField);
$result = HasManyList::create($componentClass, $joinField);
if ($this->model) {
$result->setDataModel($this->model);
}
return $result
->setDataQueryParam($this->getInheritableQueryParams())
->forForeignID($this->ID);
}
case 'belongs_to':
case 'has_many': {
// These relations must have a has_one on the other end, so find it
$joinField = $remote->getRemoteJoinField($remoteRelation, $relationType, $polymorphic);
if ($polymorphic) {
throw new InvalidArgumentException(sprintf(
"%s cannot generate opposite component of relation %s.%s, as the other end appears" .
"to be a has_one polymorphic. This method does not support polymorphic relationships",
__METHOD__,
$remoteClass,
$remoteRelation
));
}
$joinID = $this->getField($joinField);
if (empty($joinID)) {
return null;
}
// Get object by joined ID
return DataObject::get($remoteClass)
->filter('ID', $joinID)
->setDataQueryParam($this->getInheritableQueryParams())
->first();
}
case 'many_many':
case 'belongs_many_many': {
// Get components and extra fields from parent
list($componentClass, $parentClass, $componentField, $parentField, $table)
= $remote->manyManyComponent($remoteRelation);
$extraFields = $remote->manyManyExtraFieldsForComponent($remoteRelation) ?: array();
// Reverse parent and component fields and create an inverse ManyManyList
/** @var ManyManyList $result */
$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
if($this->model) {
$result->setDataModel($this->model);
}
$this->extend('updateManyManyComponents', $result);
// If this is called on a singleton, then we return an 'orphaned relation' that can have the
// foreignID set elsewhere.
return $result
->setDataQueryParam($this->getInheritableQueryParams())
->forForeignID($this->ID);
}
default: {
return null;
}
}
}
/**
* Tries to find the database key on another object that is used to store a
* relationship to this class. If no join field can be found it defaults to 'ParentID'.

View File

@ -944,12 +944,112 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
* @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);
public function findOwners($recursive = true, $list = null) {
if (!$list) {
$list = new ArrayList();
}
// Build reverse lookup for ownership
// @todo - Cache this more intelligently
$rules = $this->lookupReverseOwners();
// Hand off to recursive method
return $this->findOwnersRecursive($recursive, $list, $rules);
}
/**
* 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 List to add items to
* @param array $lookup List of reverse lookup rules for owned objects
* @return ArrayList list of objects
*/
public function findOwnersRecursive($recursive, $list, $lookup) {
// First pass: find objects that are explicitly owned_by (e.g. custom relationships)
$owners = $this->findRelatedObjects('owned_by', false);
// Second pass: Find owners via reverse lookup list
foreach($lookup as $ownedClass => $classLookups) {
// Skip owners of other objects
if(!is_a($this->owner, $ownedClass)) {
continue;
}
foreach($classLookups as $classLookup) {
// Merge new owners into this object's owners
$ownerClass = $classLookup['class'];
$ownerRelation = $classLookup['relation'];
$result = $this->owner->inferReciprocalComponent($ownerClass, $ownerRelation);
$this->mergeRelatedObjects($owners, $result);
}
}
// Merge all objects into the main list
$newItems = $this->mergeRelatedObjects($list, $owners);
// If recursing, iterate over all newly added items
if($recursive) {
foreach($newItems as $item) {
/** @var Versioned|DataObject $item */
$item->findOwnersRecursive(true, $list, $lookup);
}
}
return $list;
}
/**
* Find a list of classes, each of which with a list of methods to invoke
* to lookup owners.
*
* @return array
*/
protected function lookupReverseOwners() {
// Find all classes with 'owns' config
$lookup = array();
foreach(ClassInfo::subclassesFor(DataObject::class) as $class) {
// Ensure this class is versioned
if(!Object::has_extension($class, Versioned::class)) {
continue;
}
// Check owned objects for this class
$owns = Config::inst()->get($class, 'owns', Config::UNINHERITED);
if(empty($owns)) {
continue;
}
/** @var DataObject $instance */
$instance = $class::singleton();
foreach($owns as $owned) {
// Find owned class
$ownedClass = $instance->getRelationClass($owned);
// Skip custom methods that don't have db relationsm
if(!$ownedClass) {
continue;
}
if($ownedClass === 'DataObject') {
throw new LogicException(sprintf(
"Relation %s on class %s cannot be owned as it is polymorphic",
$owned, $class
));
}
// Add lookup for owned class
if(!isset($lookup[$ownedClass])) {
$lookup[$ownedClass] = array();
}
$lookup[$ownedClass][] = [
'class' => $class,
'relation' => $owned
];
}
}
return $lookup;
}
/**
* Find objects in the given relationships, merging them into the given list
*
@ -985,33 +1085,55 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
// Inspect value of this relationship
$items = $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;
// Merge any new item
$newItems = $this->mergeRelatedObjects($list, $items);
// 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) {
// Recurse if necessary
if($recursive) {
foreach($newItems as $item) {
/** @var Versioned|DataObject $item */
$item->findRelatedObjects($source, true, $list);
};
}
}
}
return $list;
}
/**
* Helper method to merge owned/owning items into a list.
* Items already present in the list will be skipped.
*
* @param ArrayList $list Items to merge into
* @param mixed $items List of new items to merge
* @return ArrayList List of all newly added items that did not already exist in $list
*/
protected function mergeRelatedObjects($list, $items) {
$added = new ArrayList();
if(!$items) {
return $added;
}
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;
$added[$itemKey] = $item;
}
return $added;
}
/**
* 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.
@ -2070,6 +2192,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
public function onAfterRollback($version) {
// Find record at this version
$baseClass = ClassInfo::baseDataClass($this->owner);
/** @var Versioned|DataObject $recordVersion */
$recordVersion = static::get_version($baseClass, $this->owner->ID, $version);
// Note that unlike other publishing actions, rollback is NOT recursive;

View File

@ -220,6 +220,39 @@ class DataExtensionTest extends SapphireTest {
$this->assertNotEmpty($fields->dataFieldByName('ChildField'));
$this->assertNotEmpty($fields->dataFieldByName('GrandchildField'));
}
/**
* Test setOwner behaviour
*/
public function testSetOwner() {
$extension = new DataExtensionTest_Ext1();
$obj1 = $this->objFromFixture('DataExtensionTest_RelatedObject', 'obj1');
$obj2 = $this->objFromFixture('DataExtensionTest_RelatedObject', 'obj1');
$extension->setOwner(null);
$this->assertNull($extension->getOwner());
// Set original owner
$extension->setOwner($obj1);
$this->assertEquals($obj1, $extension->getOwner());
// Set nested owner
$extension->setOwner($obj2);
$this->assertEquals($obj2, $extension->getOwner());
// Clear nested owner
$extension->clearOwner();
$this->assertEquals($obj1, $extension->getOwner());
// Clear original owner
$extension->clearOwner();
$this->assertNull($extension->getOwner());
// Another clearOwner should error
$this->setExpectedException("BadMethodCallException", "clearOwner() called more than setOwner()");
$extension->clearOwner();
}
}
class DataExtensionTest_Member extends DataObject implements TestOnly {

View File

@ -318,6 +318,12 @@ class DataObjectTest extends SapphireTest {
// There will be a method called $obj->relname() that returns the object itself
$this->assertEquals($team1ID, $captain1->FavouriteTeam()->ID);
// Test that getNonReciprocalComponent can find has_one from the has_many end
$this->assertEquals(
$team1ID,
$captain1->inferReciprocalComponent('DataObjectTest_Team', 'PlayerFans')->ID
);
// Check entity with polymorphic has-one
$fan1 = $this->objFromFixture("DataObjectTest_Fan", "fan1");
$this->assertTrue((bool)$fan1->hasValue('Favourite'));
@ -408,10 +414,19 @@ class DataObjectTest extends SapphireTest {
// Test getComponents() gets the ComponentSet of the other side of the relation
$this->assertTrue($team1->Comments()->Count() == 2);
$team1Comments = [
['Comment' => 'This is a team comment by Joe'],
['Comment' => 'This is a team comment by Bob'],
];
// Test the IDs on the DataObjects are set correctly
foreach($team1->Comments() as $comment) {
$this->assertEquals($team1->ID, $comment->TeamID);
}
$this->assertDOSEquals($team1Comments, $team1->Comments());
// Test that has_many can be infered from the has_one via getNonReciprocalComponent
$this->assertDOSEquals(
$team1Comments,
$team1->inferReciprocalComponent('DataObjectTest_TeamComment', 'Team')
);
// Test that we can add and remove items that already exist in the database
$newComment = new DataObjectTest_TeamComment();
@ -1220,6 +1235,7 @@ class DataObjectTest extends SapphireTest {
public function testMultipleManyManyWithSameClass() {
$team = $this->objFromFixture('DataObjectTest_Team', 'team1');
$company2 = $this->objFromFixture('DataObjectTest_EquipmentCompany', 'equipmentcompany2');
$sponsors = $team->Sponsors();
$equipmentSuppliers = $team->EquipmentSuppliers();
@ -1241,6 +1257,25 @@ class DataObjectTest extends SapphireTest {
$this->assertInstanceOf('ManyManyList', $teamWithoutSponsor->Sponsors());
$this->assertEquals(0, $teamWithoutSponsor->Sponsors()->count());
// Test that belongs_many_many can be infered from with getNonReciprocalComponent
$this->assertDOSEquals(
[
['Name' => 'Company corp'],
['Name' => 'Team co.'],
],
$team->inferReciprocalComponent('DataObjectTest_EquipmentCompany', 'SponsoredTeams')
);
// Test that many_many can be infered from getNonReciprocalComponent
$this->assertDOSEquals(
[
['Title' => 'Team 1'],
['Title' => 'Team 2'],
['Title' => 'Subteam 1'],
],
$company2->inferReciprocalComponent('DataObjectTest_Team', 'Sponsors')
);
// Check many_many_extraFields still works
$equipmentCompany = $this->objFromFixture('DataObjectTest_EquipmentCompany', 'equipmentcompany1');
$equipmentCompany->SponsoredTeams()->add($teamWithoutSponsor, array('SponsorFee' => 1000));
@ -1269,6 +1304,7 @@ class DataObjectTest extends SapphireTest {
$subTeam->Sponsors()->add($subEquipmentCompany, array('SponsorFee' => 1200));
$this->assertEquals(1200, $subTeam->Sponsors()->byID($subEquipmentCompany->ID)->SponsorFee,
'Data from inherited many_many_extraFields was not stored/extracted correctly');
}
public function testManyManyExtraFields() {
@ -1537,6 +1573,7 @@ class DataObjectTest extends SapphireTest {
$company = new DataObjectTest_Company();
$ceo = new DataObjectTest_CEO();
$company->Name = 'New Company';
$company->write();
$ceo->write();
@ -1546,6 +1583,19 @@ class DataObjectTest extends SapphireTest {
$this->assertEquals($company->ID, $ceo->Company()->ID, 'belongs_to returns the right results.');
// Test belongs_to can be infered via getNonReciprocalComponent
// Note: Will be returned as has_many since the belongs_to is ignored.
$this->assertDOSEquals(
[['Name' => 'New Company']],
$ceo->inferReciprocalComponent('DataObjectTest_Company', 'CEO')
);
// Test has_one to a belongs_to can be infered via getNonReciprocalComponent
$this->assertEquals(
$ceo->ID,
$company->inferReciprocalComponent('DataObjectTest_CEO', 'Company')->ID
);
// Test automatic creation of class where no assigment exists
$ceo = new DataObjectTest_CEO();
$ceo->write();
@ -1749,7 +1799,8 @@ class DataObjectTest_Team extends DataObject implements TestOnly {
private static $has_many = array(
'SubTeams' => 'DataObjectTest_SubTeam',
'Comments' => 'DataObjectTest_TeamComment',
'Fans' => 'DataObjectTest_Fan.Favourite' // Polymorphic - Team fans
'Fans' => 'DataObjectTest_Fan.Favourite', // Polymorphic - Team fans
'PlayerFans' => 'DataObjectTest_Player.FavouriteTeam'
);
private static $many_many = array(

View File

@ -11,12 +11,15 @@ class VersionedOwnershipTest extends SapphireTest {
'VersionedOwnershipTest_Related',
'VersionedOwnershipTest_Attachment',
'VersionedOwnershipTest_RelatedMany',
'VersionedOwnershipTest_Page',
'VersionedOwnershipTest_Banner',
'VersionedOwnershipTest_Image',
'VersionedOwnershipTest_CustomRelation',
);
protected static $fixture_file = 'VersionedOwnershipTest.yml';
public function setUp()
{
public function setUp() {
parent::setUp();
Versioned::set_stage(Versioned::DRAFT);
@ -27,7 +30,7 @@ class VersionedOwnershipTest extends SapphireTest {
if(stripos($name, '_published') !== false) {
/** @var Versioned|DataObject $object */
$object = DataObject::get($class)->byID($id);
$object->publish('Stage', 'Live');
$object->publish(Versioned::DRAFT, Versioned::LIVE);
}
}
}
@ -504,6 +507,80 @@ class VersionedOwnershipTest extends SapphireTest {
);
}
/**
* Test that you can find owners without owned_by being defined explicitly
*/
public function testInferedOwners() {
// Make sure findOwned() works
/** @var VersionedOwnershipTest_Page $page1 */
$page1 = $this->objFromFixture('VersionedOwnershipTest_Page', 'page1_published');
/** @var VersionedOwnershipTest_Page $page2 */
$page2 = $this->objFromFixture('VersionedOwnershipTest_Page', 'page2_published');
$this->assertDOSEquals(
[
['Title' => 'Banner 1'],
['Title' => 'Image 1'],
['Title' => 'Custom 1'],
],
$page1->findOwned()
);
$this->assertDOSEquals(
[
['Title' => 'Banner 2'],
['Title' => 'Banner 3'],
['Title' => 'Image 1'],
['Title' => 'Image 2'],
['Title' => 'Custom 2'],
],
$page2->findOwned()
);
// Check that findOwners works
/** @var VersionedOwnershipTest_Image $image1 */
$image1 = $this->objFromFixture('VersionedOwnershipTest_Image', 'image1_published');
/** @var VersionedOwnershipTest_Image $image2 */
$image2 = $this->objFromFixture('VersionedOwnershipTest_Image', 'image2_published');
$this->assertDOSEquals(
[
['Title' => 'Banner 1'],
['Title' => 'Banner 2'],
['Title' => 'Page 1'],
['Title' => 'Page 2'],
],
$image1->findOwners()
);
$this->assertDOSEquals(
[
['Title' => 'Banner 1'],
['Title' => 'Banner 2'],
],
$image1->findOwners(false)
);
$this->assertDOSEquals(
[
['Title' => 'Banner 3'],
['Title' => 'Page 2'],
],
$image2->findOwners()
);
$this->assertDOSEquals(
[
['Title' => 'Banner 3'],
],
$image2->findOwners(false)
);
// Test custom relation can findOwners()
/** @var VersionedOwnershipTest_CustomRelation $custom1 */
$custom1 = $this->objFromFixture('VersionedOwnershipTest_CustomRelation', 'custom1_published');
$this->assertDOSEquals(
[['Title' => 'Page 1']],
$custom1->findOwners()
);
}
}
/**
@ -622,3 +699,107 @@ class VersionedOwnershipTest_Attachment extends DataObject implements TestOnly {
'AttachedTo'
);
}
/**
* Page which owns a lits of banners
*
* @mixin Versioned
*/
class VersionedOwnershipTest_Page extends DataObject implements TestOnly {
private static $extensions = array(
'Versioned',
);
private static $db = array(
'Title' => 'Varchar(255)',
);
private static $many_many = array(
'Banners' => 'VersionedOwnershipTest_Banner',
);
private static $owns = array(
'Banners',
'Custom'
);
/**
* All custom objects with the same number. E.g. 'Page 1' owns 'Custom 1'
*
* @return DataList
*/
public function Custom() {
$title = str_replace('Page', 'Custom', $this->Title);
return VersionedOwnershipTest_CustomRelation::get()
->filter('Title', $title);
}
}
/**
* Banner which doesn't declare its belongs_many_many, but owns an Image
*
* @mixin Versioned
*/
class VersionedOwnershipTest_Banner extends DataObject implements TestOnly {
private static $extensions = array(
'Versioned',
);
private static $db = array(
'Title' => 'Varchar(255)',
);
private static $has_one = array(
'Image' => 'VersionedOwnershipTest_Image',
);
private static $owns = array(
'Image',
);
}
/**
* Object which is owned via a custom PHP method rather than DB relation
*
* @mixin Versioned
*/
class VersionedOwnershipTest_CustomRelation extends DataObject implements TestOnly {
private static $extensions = array(
'Versioned',
);
private static $db = array(
'Title' => 'Varchar(255)',
);
private static $owned_by = array(
'Pages'
);
/**
* All pages with the same number. E.g. 'Page 1' owns 'Custom 1'
*
* @return DataList
*/
public function Pages() {
$title = str_replace('Custom', 'Page', $this->Title);
return VersionedOwnershipTest_Page::get()->filter('Title', $title);
}
}
/**
* Simple versioned dataobject
*
* @mixin Versioned
*/
class VersionedOwnershipTest_Image extends DataObject implements TestOnly {
private static $extensions = array(
'Versioned',
);
private static $db = array(
'Title' => 'Varchar(255)',
);
}

View File

@ -43,3 +43,34 @@ VersionedOwnershipTest_RelatedMany:
VersionedOwnershipTest_Object:
object1:
Title: 'Object 1'
VersionedOwnershipTest_Image:
image1_published:
Title: 'Image 1'
image2_published:
Title: 'Image 2'
VersionedOwnershipTest_Banner:
banner1_published:
Title: 'Banner 1'
Image: =>VersionedOwnershipTest_Image.image1_published
banner2_published:
Title: 'Banner 2'
Image: =>VersionedOwnershipTest_Image.image1_published
banner3_published:
Title: 'Banner 3'
Image: =>VersionedOwnershipTest_Image.image2_published
VersionedOwnershipTest_Page:
page1_published:
Title: 'Page 1'
Banners: =>VersionedOwnershipTest_Banner.banner1_published
page2_published:
Title: 'Page 2'
Banners: =>VersionedOwnershipTest_Banner.banner2_published,=>VersionedOwnershipTest_Banner.banner3_published
VersionedOwnershipTest_CustomRelation:
custom1_published:
Title: 'Custom 1'
custom2_published:
Title: 'Custom 2'