mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
API Formally support custom ownership relations
API 'owned_by' is no longer mandatory for relations backed by normal db relations API Extension setOwner/clearOwner is now nested
This commit is contained in:
parent
633eb0163e
commit
094745ec0f
@ -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];
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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'.
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -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)',
|
||||
);
|
||||
}
|
||||
|
@ -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'
|
||||
|
Loading…
Reference in New Issue
Block a user