mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
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:
parent
1ea14382ee
commit
257ff69e32
@ -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
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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 */
|
||||||
|
@ -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"
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
36
tests/php/ORM/ManyManyThroughListTest/PolyItem.php
Normal file
36
tests/php/ORM/ManyManyThroughListTest/PolyItem.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
tests/php/ORM/ManyManyThroughListTest/PolyJoinObject.php
Normal file
21
tests/php/ORM/ManyManyThroughListTest/PolyJoinObject.php
Normal 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,
|
||||||
|
];
|
||||||
|
}
|
28
tests/php/ORM/ManyManyThroughListTest/PolyObjectA.php
Normal file
28
tests/php/ORM/ManyManyThroughListTest/PolyObjectA.php
Normal 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',
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
28
tests/php/ORM/ManyManyThroughListTest/PolyObjectB.php
Normal file
28
tests/php/ORM/ManyManyThroughListTest/PolyObjectB.php
Normal 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',
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
@ -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
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user