API Implement many_many through polymorphic (from only) (#7928)

* API Support many_many through polymorphic relations (from side only)
Fixes #7911
Fixes #3136

* Add extra docs and allow optional arguments

* ENHANCEMENT Enable quiet to be turned off

* BUG Fix issue with manymanythroughlist duplication
This commit is contained in:
Damian Mooyman 2018-03-22 10:26:25 +13:00 committed by Aaron Carlino
parent 1ea14382ee
commit 257ff69e32
16 changed files with 470 additions and 32 deletions

View File

@ -339,6 +339,59 @@ $supporters = $team->Supporters()->where(['"TeamSupporter"."Ranking"' => 1]);
Note: ->filter() currently does not support joined fields natively due to the fact that the Note: ->filter() currently does not support joined fields natively due to the fact that the
query for the join table is isolated from the outer query controlled by DataList. query for the join table is isolated from the outer query controlled by DataList.
### Polymorphic many_many (Experimental)
Using many_many through, it is possible to support polymorphic relations on the mapping table.
Note, that this feature is currently experimental, and has certain limitations:
- This feature only works with many_many through
- This feature will only allow polymorphic many_many, but not belongs_many_many. However,
you can have a has_many relation to the mapping table on this side, and iterate through this
to collate parent records.
For instance, this is how you would link an arbitrary object to many_many tags.
```php
use SilverStripe\ORM\DataObject;
class SomeObject extends DataObject
{
// This same many_many may also exist on other classes
private static $many_many = [
"Tags" => [
'through' => TagMapping::class,
'from' => 'Parent',
'to' => 'Tag',
]
];
}
class Tag extends DataObject
{
// has_many works, but belongs_many_many will not
private static $has_many = [
'TagMappings' => TagMapping::class,
];
/**
* Example iterator placeholder for belongs_many_many.
* This is a list of arbitrary types of objects
* @return Generator|DataObject[]
*/
public function TaggedObjects()
{
foreach ($this->TagMappings() as $mapping) {
yield $mapping->Parent();
}
}
}
class TagMapping extends DataObject
{
private static $has_one = [
'Parent' => DataObject::class, // Polymorphic has_one
'Tag' => Tag::class,
];
}
```
### Using many_many in templates ### Using many_many in templates

View File

@ -98,10 +98,12 @@ abstract class DBSchemaManager
/** /**
* Enable supression of database messages. * Enable supression of database messages.
*
* @param bool $quiet
*/ */
public function quiet() public function quiet($quiet = true)
{ {
$this->supressOutput = true; $this->supressOutput = $quiet;
} }
/** /**

View File

@ -678,10 +678,12 @@ class DB
/** /**
* Enable supression of database messages. * Enable supression of database messages.
*
* @param bool $quiet
*/ */
public static function quiet() public static function quiet($quiet = true)
{ {
self::get_schema()->quiet(); self::get_schema()->quiet($quiet);
} }
/** /**

View File

@ -104,6 +104,7 @@ use stdClass;
* @property string $ClassName Class name of the DataObject * @property string $ClassName Class name of the DataObject
* @property string $LastEdited Date and time of DataObject's last modification. * @property string $LastEdited Date and time of DataObject's last modification.
* @property string $Created Date and time of DataObject creation. * @property string $Created Date and time of DataObject creation.
* @property string $ObsoleteClassName If ClassName no longer exists this will be set to the legacy value
*/ */
class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider, Resettable class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider, Resettable
{ {
@ -1982,7 +1983,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$manyMany['join'], $manyMany['join'],
$manyMany['parentField'], // Reversed parent / child field $manyMany['parentField'], // Reversed parent / child field
$manyMany['childField'], // Reversed parent / child field $manyMany['childField'], // Reversed parent / child field
$extraFields $extraFields,
$manyMany['childClass'], // substitute child class for parentClass
$remoteClass // In case ManyManyThroughList needs to use PolymorphicHasManyList internally
); );
$this->extend('updateManyManyComponents', $result); $this->extend('updateManyManyComponents', $result);
@ -2040,10 +2043,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$manyManyComponent['join'], $manyManyComponent['join'],
$manyManyComponent['childField'], $manyManyComponent['childField'],
$manyManyComponent['parentField'], $manyManyComponent['parentField'],
$extraFields $extraFields,
$manyManyComponent['parentClass'],
static::class // In case ManyManyThroughList needs to use PolymorphicHasManyList internally
); );
// Store component data in query meta-data // Store component data in query meta-data
$result = $result->alterDataQuery(function ($query) use ($extraFields) { $result = $result->alterDataQuery(function ($query) use ($extraFields) {
/** @var DataQuery $query */ /** @var DataQuery $query */

View File

@ -940,7 +940,8 @@ class DataObjectSchema
'relationClass' => ManyManyThroughList::class, 'relationClass' => ManyManyThroughList::class,
'parentClass' => $parentClass, 'parentClass' => $parentClass,
'childClass' => $joinChildClass, 'childClass' => $joinChildClass,
'parentField' => $specification['from'] . 'ID', /** @internal Polymorphic many_many is experimental */
'parentField' => $specification['from'] . ($parentClass === DataObject::class ? '' : 'ID'),
'childField' => $specification['to'] . 'ID', 'childField' => $specification['to'] . 'ID',
'join' => $joinClass, 'join' => $joinClass,
]; ];
@ -982,10 +983,18 @@ class DataObjectSchema
if (!$otherManyMany) { if (!$otherManyMany) {
return null; return null;
} }
foreach ($otherManyMany as $inverseComponentName => $nextClass) { foreach ($otherManyMany as $inverseComponentName => $manyManySpec) {
if ($nextClass === $parentClass) { // Normal many-many
if ($manyManySpec === $parentClass) {
return $inverseComponentName; return $inverseComponentName;
} }
// many-many through, inspect 'to' for the many_many
if (is_array($manyManySpec)) {
$toClass = $this->hasOneComponent($manyManySpec['through'], $manyManySpec['to']);
if ($toClass === $parentClass) {
return $inverseComponentName;
}
}
} }
return null; return null;
} }
@ -1106,7 +1115,13 @@ class DataObjectSchema
} }
// Check for polymorphic // Check for polymorphic
/** @internal Polymorphic many_many is experimental */
if ($relationClass === DataObject::class) { if ($relationClass === DataObject::class) {
// Currently polymorphic 'from' is supported.
if ($key === 'from') {
return $relationClass;
}
// @todo support polymorphic 'to'
throw new InvalidArgumentException( throw new InvalidArgumentException(
"many_many through relation {$parentClass}.{$component} {$key} references a polymorphic field " "many_many through relation {$parentClass}.{$component} {$key} references a polymorphic field "
. "{$joinClass}::{$relation} which is not supported" . "{$joinClass}::{$relation} which is not supported"

View File

@ -4,6 +4,7 @@ namespace SilverStripe\ORM;
use BadMethodCallException; use BadMethodCallException;
use InvalidArgumentException; use InvalidArgumentException;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
/** /**
@ -25,14 +26,30 @@ class ManyManyThroughList extends RelationList
* @param string $localKey The key in the join table that maps to the dataClass' PK. * @param string $localKey The key in the join table that maps to the dataClass' PK.
* @param string $foreignKey The key in the join table that maps to joined class' PK. * @param string $foreignKey The key in the join table that maps to joined class' PK.
* *
* @param array $extraFields Ignored for ManyManyThroughList
* @param string $foreignClass 'from' class
* @param string $parentClass Parent class (should be subclass of 'from')
* @example new ManyManyThroughList('Banner', 'PageBanner', 'BannerID', 'PageID'); * @example new ManyManyThroughList('Banner', 'PageBanner', 'BannerID', 'PageID');
*/ */
public function __construct($dataClass, $joinClass, $localKey, $foreignKey) public function __construct(
{ $dataClass,
$joinClass,
$localKey,
$foreignKey,
$extraFields = [],
$foreignClass = null,
$parentClass = null
) {
parent::__construct($dataClass); parent::__construct($dataClass);
// Inject manipulator // Inject manipulator
$this->manipulator = ManyManyThroughQueryManipulator::create($joinClass, $localKey, $foreignKey); $this->manipulator = ManyManyThroughQueryManipulator::create(
$joinClass,
$localKey,
$foreignKey,
$foreignClass,
$parentClass
);
$this->dataQuery->pushQueryManipulator($this->manipulator); $this->dataQuery->pushQueryManipulator($this->manipulator);
} }
@ -64,7 +81,7 @@ class ManyManyThroughList extends RelationList
} }
// Create parent record // Create parent record
$record = parent::createDataObject($row); $record = parent::createDataObject($row);
// Create joined record // Create joined record
if ($joinRow) { if ($joinRow) {
@ -145,6 +162,10 @@ class ManyManyThroughList extends RelationList
if (is_numeric($item)) { if (is_numeric($item)) {
$itemID = $item; $itemID = $item;
} elseif ($item instanceof $this->dataClass) { } elseif ($item instanceof $this->dataClass) {
/** @var DataObject $item */
if (!$item->isInDB()) {
$item->write();
}
$itemID = $item->ID; $itemID = $item->ID;
} else { } else {
throw new InvalidArgumentException( throw new InvalidArgumentException(
@ -152,7 +173,7 @@ class ManyManyThroughList extends RelationList
); );
} }
if (empty($itemID)) { if (empty($itemID)) {
throw new InvalidArgumentException("ManyManyThroughList::add() doesn't accept unsaved records"); throw new InvalidArgumentException("ManyManyThroughList::add() could not add record without ID");
} }
// Validate foreignID // Validate foreignID
@ -169,7 +190,8 @@ class ManyManyThroughList extends RelationList
// Update existing records // Update existing records
$localKey = $this->manipulator->getLocalKey(); $localKey = $this->manipulator->getLocalKey();
$foreignKey = $this->manipulator->getForeignKey(); // Foreign key (or key for ID field if polymorphic)
$foreignKey = $this->manipulator->getForeignIDKey();
$hasManyList = $this->manipulator->getParentRelationship($this->dataQuery()); $hasManyList = $this->manipulator->getParentRelationship($this->dataQuery());
$records = $hasManyList->filter($localKey, $itemID); $records = $hasManyList->filter($localKey, $itemID);
/** @var DataObject $record */ /** @var DataObject $record */
@ -185,12 +207,27 @@ class ManyManyThroughList extends RelationList
unset($foreignIDsToAdd[$foreignID]); unset($foreignIDsToAdd[$foreignID]);
} }
// Once existing records are updated, add missing mapping records // Check if any records remain to add
foreach ($foreignIDsToAdd as $foreignID) { if (empty($foreignIDsToAdd)) {
$record = $hasManyList->createDataObject($extraFields ?: []); return;
$record->$foreignKey = $foreignID;
$record->$localKey = $itemID;
$record->write();
} }
// Add item to relation
$hasManyList = $hasManyList->forForeignID($foreignIDsToAdd);
$record = $hasManyList->createDataObject($extraFields ?: []);
$record->$localKey = $itemID;
$hasManyList->add($record);
}
/**
* Get extra fields used by this list
*
* @return array a map of field names to types
*/
public function getExtraFields()
{
// Inherit config from join table
$joinClass = $this->manipulator->getJoinClass();
return Config::inst()->get($joinClass, 'db');
} }
} }

View File

@ -5,6 +5,7 @@ namespace SilverStripe\ORM;
use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\Queries\SQLSelect; use SilverStripe\ORM\Queries\SQLSelect;
/** /**
@ -36,6 +37,20 @@ class ManyManyThroughQueryManipulator implements DataQueryManipulator
*/ */
protected $foreignKey; protected $foreignKey;
/**
* Foreign class 'from' property. Normally not needed unless polymorphic.
*
* @var string
*/
protected $foreignClass;
/**
* Class name of instance that owns this list
*
* @var string
*/
protected $parentClass;
/** /**
* Build query manipulator for a given join table. Additional parameters (foreign key, etc) * Build query manipulator for a given join table. Additional parameters (foreign key, etc)
* will be infered at evaluation from query parameters set via the ManyManyThroughList * will be infered at evaluation from query parameters set via the ManyManyThroughList
@ -43,12 +58,24 @@ class ManyManyThroughQueryManipulator implements DataQueryManipulator
* @param string $joinClass Class name of the joined dataobject record * @param string $joinClass Class name of the joined dataobject record
* @param string $localKey The key in the join table that maps to the dataClass' PK. * @param string $localKey The key in the join table that maps to the dataClass' PK.
* @param string $foreignKey The key in the join table that maps to joined class' PK. * @param string $foreignKey The key in the join table that maps to joined class' PK.
* @param string $foreignClass the 'from' class name
* @param string $parentClass Name of parent class. Subclass of $foreignClass
*/ */
public function __construct($joinClass, $localKey, $foreignKey) public function __construct($joinClass, $localKey, $foreignKey, $foreignClass = null, $parentClass = null)
{ {
$this->setJoinClass($joinClass); $this->setJoinClass($joinClass);
$this->setLocalKey($localKey); $this->setLocalKey($localKey);
$this->setForeignKey($foreignKey); $this->setForeignKey($foreignKey);
if ($foreignClass) {
$this->setForeignClass($foreignClass);
} else {
Deprecation::notice('5.0', 'Arg $foreignClass will be mandatory in 5.x');
}
if ($parentClass) {
$this->setParentClass($parentClass);
} else {
Deprecation::notice('5.0', 'Arg $parentClass will be mandatory in 5.x');
}
} }
/** /**
@ -95,6 +122,33 @@ class ManyManyThroughQueryManipulator implements DataQueryManipulator
return $this->foreignKey; return $this->foreignKey;
} }
/**
* Gets ID key name for foreign key component
*
* @return string
*/
public function getForeignIDKey()
{
$key = $this->getForeignKey();
if ($this->getForeignClass() === DataObject::class) {
return $key . 'ID';
}
return $key;
}
/**
* Gets Class key name for foreign key component (or null if none)
*
* @return string|null
*/
public function getForeignClassKey()
{
if ($this->getForeignClass() === DataObject::class) {
return $this->getForeignKey() . 'Class';
}
return null;
}
/** /**
* @param string $foreignKey * @param string $foreignKey
* @return $this * @return $this
@ -114,11 +168,21 @@ class ManyManyThroughQueryManipulator implements DataQueryManipulator
public function getParentRelationship(DataQuery $query) public function getParentRelationship(DataQuery $query)
{ {
// Create has_many // Create has_many
$list = HasManyList::create($this->getJoinClass(), $this->getForeignKey()); if ($this->getForeignClass() === DataObject::class) {
/** @internal Polymorphic many_many is experimental */
$list = PolymorphicHasManyList::create(
$this->getJoinClass(),
$this->getForeignKey(),
$this->getParentClass()
);
} else {
$list = HasManyList::create($this->getJoinClass(), $this->getForeignKey());
}
$list = $list->setDataQueryParam($this->extractInheritableQueryParameters($query)); $list = $list->setDataQueryParam($this->extractInheritableQueryParameters($query));
// Limit to given foreign key // Limit to given foreign key
if ($foreignID = $query->getQueryParam('Foreign.ID')) { $foreignID = $query->getQueryParam('Foreign.ID');
if ($foreignID) {
$list = $list->forForeignID($foreignID); $list = $list->forForeignID($foreignID);
} }
return $list; return $list;
@ -190,6 +254,8 @@ class ManyManyThroughQueryManipulator implements DataQueryManipulator
} }
// Apply join and record sql for later insertion (at end of replacements) // Apply join and record sql for later insertion (at end of replacements)
// By using a string placeholder $$_SUBQUERY_$$ we protect field/table rewrites from interfering twice
// on the already-finalised inner list
$sqlSelect->addInnerJoin( $sqlSelect->addInnerJoin(
'(SELECT $$_SUBQUERY_$$)', '(SELECT $$_SUBQUERY_$$)',
"\"{$joinTableAlias}\".\"{$localKey}\" = {$childField}", "\"{$joinTableAlias}\".\"{$localKey}\" = {$childField}",
@ -223,4 +289,40 @@ class ManyManyThroughQueryManipulator implements DataQueryManipulator
$dataQuery->setQueryParam('Foreign.JoinTableSQL', null); $dataQuery->setQueryParam('Foreign.JoinTableSQL', null);
} }
} }
/**
* @return string
*/
public function getForeignClass()
{
return $this->foreignClass;
}
/**
* @param string $foreignClass
* @return $this
*/
public function setForeignClass($foreignClass)
{
$this->foreignClass = $foreignClass;
return $this;
}
/**
* @return string
*/
public function getParentClass()
{
return $this->parentClass;
}
/**
* @param string $parentClass
* @return ManyManyThroughQueryManipulator
*/
public function setParentClass($parentClass)
{
$this->parentClass = $parentClass;
return $this;
}
} }

View File

@ -67,9 +67,8 @@ class PolymorphicHasManyList extends HasManyList
if (is_numeric($item)) { if (is_numeric($item)) {
$item = DataObject::get_by_id($this->dataClass, $item); $item = DataObject::get_by_id($this->dataClass, $item);
} elseif (!($item instanceof $this->dataClass)) { } elseif (!($item instanceof $this->dataClass)) {
user_error( throw new InvalidArgumentException(
"PolymorphicHasManyList::add() expecting a $this->dataClass object, or ID value", "PolymorphicHasManyList::add() expecting a $this->dataClass object, or ID value"
E_USER_ERROR
); );
} }

View File

@ -5,7 +5,7 @@ namespace SilverStripe\ORM\Tests;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ManyManyThroughList; use SilverStripe\ORM\ManyManyThroughList;
use InvalidArgumentException; use SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyItem;
class ManyManyThroughListTest extends SapphireTest class ManyManyThroughListTest extends SapphireTest
{ {
@ -14,7 +14,11 @@ class ManyManyThroughListTest extends SapphireTest
protected static $extra_dataobjects = [ protected static $extra_dataobjects = [
ManyManyThroughListTest\Item::class, ManyManyThroughListTest\Item::class,
ManyManyThroughListTest\JoinObject::class, ManyManyThroughListTest\JoinObject::class,
ManyManyThroughListTest\TestObject::class ManyManyThroughListTest\TestObject::class,
ManyManyThroughListTest\PolyItem::class,
ManyManyThroughListTest\PolyJoinObject::class,
ManyManyThroughListTest\PolyObjectA::class,
ManyManyThroughListTest\PolyObjectB::class,
]; ];
protected function setUp() protected function setUp()
@ -185,4 +189,76 @@ class ManyManyThroughListTest extends SapphireTest
$schema->manyManyComponent(ManyManyThroughListTest\Item::class, 'Objects') $schema->manyManyComponent(ManyManyThroughListTest\Item::class, 'Objects')
); );
} }
/**
* Note: polymorphic many_many support is currently experimental
*/
public function testPolymorphicManyMany()
{
/** @var ManyManyThroughListTest\PolyObjectA $objA1 */
$objA1 = $this->objFromFixture(ManyManyThroughListTest\PolyObjectA::class, 'obja1');
/** @var ManyManyThroughListTest\PolyObjectB $objB1 */
$objB1 = $this->objFromFixture(ManyManyThroughListTest\PolyObjectB::class, 'objb1');
/** @var ManyManyThroughListTest\PolyObjectB $objB2 */
$objB2 = $this->objFromFixture(ManyManyThroughListTest\PolyObjectB::class, 'objb2');
// Test various parent class queries
$this->assertListEquals([
['Title' => 'item 1'],
['Title' => 'item 2'],
], $objA1->Items());
$this->assertListEquals([
['Title' => 'item 2'],
], $objB1->Items());
$this->assertListEquals([
['Title' => 'item 2'],
], $objB2->Items());
// Test adding items
$newItem = new PolyItem();
$newItem->Title = 'New Item';
$objA1->Items()->add($newItem);
$objB2->Items()->add($newItem);
$this->assertListEquals([
['Title' => 'item 1'],
['Title' => 'item 2'],
['Title' => 'New Item'],
], $objA1->Items());
$this->assertListEquals([
['Title' => 'item 2'],
], $objB1->Items());
$this->assertListEquals([
['Title' => 'item 2'],
['Title' => 'New Item'],
], $objB2->Items());
// Test removing items
$item2 = $this->objFromFixture(ManyManyThroughListTest\PolyItem::class, 'child2');
$objA1->Items()->remove($item2);
$objB1->Items()->remove($item2);
$this->assertListEquals([
['Title' => 'item 1'],
['Title' => 'New Item'],
], $objA1->Items());
$this->assertListEquals([], $objB1->Items());
$this->assertListEquals([
['Title' => 'item 2'],
['Title' => 'New Item'],
], $objB2->Items());
// Test set-by-id-list
$objB2->Items()->setByIDList([
$newItem->ID,
$this->idFromFixture(ManyManyThroughListTest\PolyItem::class, 'child1'),
]);
$this->assertListEquals([
['Title' => 'item 1'],
['Title' => 'New Item'],
], $objA1->Items());
$this->assertListEquals([], $objB1->Items());
$this->assertListEquals([
['Title' => 'item 1'],
['Title' => 'New Item'],
], $objB2->Items());
}
} }

View File

@ -17,3 +17,37 @@ SilverStripe\ORM\Tests\ManyManyThroughListTest\JoinObject:
Sort: 2 Sort: 2
Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\TestObject.parent1 Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\TestObject.parent1
Child: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\Item.child2 Child: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\Item.child2
SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyObjectA:
obja1:
Title: 'object A1'
SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyObjectB:
objb1:
Title: 'object B1'
objb2:
Title: 'object B2'
SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyItem:
child1:
Title: 'item 1'
child2:
Title: 'item 2'
SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyJoinObject:
join1:
Title: 'join 1'
Sort: 4
Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyObjectA.obja1
Child: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyItem.child1
join2:
Title: 'join 2'
Sort: 2
Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyObjectA.obja1
Child: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyItem.child2
join3:
Title: 'join 3'
Sort: 2
Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyObjectB.objb1
Child: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyItem.child2
join4:
Title: 'join 4'
Sort: 2
Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyObjectB.objb2
Child: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyItem.child2

View File

@ -19,6 +19,7 @@ class Item extends DataObject implements TestOnly
]; ];
private static $belongs_many_many = [ private static $belongs_many_many = [
'Objects' => 'SilverStripe\\ORM\\Tests\\ManyManyThroughListTest\\TestObject.Items' // Intentionally omit parent `.Items` specifier to ensure it's not mandatory
'Objects' => TestObject::class,
]; ];
} }

View File

@ -0,0 +1,36 @@
<?php
namespace SilverStripe\ORM\Tests\ManyManyThroughListTest;
use Generator;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
class PolyItem extends DataObject implements TestOnly
{
private static $table_name = 'ManyManyThroughListTest_PolyItem';
private static $db = [
'Title' => 'Varchar'
];
private static $has_many = [
'JoinObject' => PolyJoinObject::class . '.Items',
];
/**
* Placeholder for missing belongs_many_many for polymorphic relation
*
* @todo Make this work for belongs_many_many
* @return Generator|DataObject[]
*/
public function Objects()
{
foreach ($this->JoinObject() as $object) {
$objectParent = $object->Parent();
if ($objectParent && $objectParent->exists()) {
yield $objectParent;
}
}
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace SilverStripe\ORM\Tests\ManyManyThroughListTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
class PolyJoinObject extends DataObject implements TestOnly
{
private static $table_name = 'ManyManyThroughListTest_PolyJoinObject';
private static $db = [
'Title' => 'Varchar',
'Sort' => 'Int',
];
private static $has_one = [
'Parent' => DataObject::class, // Polymorphic parent
'Child' => PolyItem::class,
];
}

View File

@ -0,0 +1,28 @@
<?php
namespace SilverStripe\ORM\Tests\ManyManyThroughListTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ManyManyThroughList;
/**
* @property string $Title
* @method ManyManyThroughList Items()
*/
class PolyObjectA extends DataObject implements TestOnly
{
private static $table_name = 'ManyManyThroughListTest_PolyObjectA';
private static $db = [
'Title' => 'Varchar'
];
private static $many_many = [
'Items' => [
'through' => PolyJoinObject::class,
'from' => 'Parent',
'to' => 'Child',
]
];
}

View File

@ -0,0 +1,28 @@
<?php
namespace SilverStripe\ORM\Tests\ManyManyThroughListTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ManyManyThroughList;
/**
* @property string $Title
* @method ManyManyThroughList Items()
*/
class PolyObjectB extends DataObject implements TestOnly
{
private static $table_name = 'ManyManyThroughListTest_PolyObjectB';
private static $db = [
'Title' => 'Varchar'
];
private static $many_many = [
'Items' => [
'through' => PolyJoinObject::class,
'from' => 'Parent',
'to' => 'Child',
]
];
}

View File

@ -10,7 +10,7 @@ use SilverStripe\ORM\ManyManyThroughList;
* Basic parent object * Basic parent object
* *
* @property string $Title * @property string $Title
* @method ManyManyThroughList Items() * @method ManyManyThroughList Items()
*/ */
class TestObject extends DataObject implements TestOnly class TestObject extends DataObject implements TestOnly
{ {