mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
NEW Allow a single has_one to manage multiple reciprocal has_many (#11084)
This commit is contained in:
parent
c890d79ea9
commit
c405ed6cf3
@ -48,6 +48,8 @@ SilverStripe\Core\Injector\Injector:
|
|||||||
class: SilverStripe\ORM\FieldType\DBPercentage
|
class: SilverStripe\ORM\FieldType\DBPercentage
|
||||||
PolymorphicForeignKey:
|
PolymorphicForeignKey:
|
||||||
class: SilverStripe\ORM\FieldType\DBPolymorphicForeignKey
|
class: SilverStripe\ORM\FieldType\DBPolymorphicForeignKey
|
||||||
|
PolymorphicRelationAwareForeignKey:
|
||||||
|
class: SilverStripe\ORM\FieldType\DBPolymorphicRelationAwareForeignKey
|
||||||
PrimaryKey:
|
PrimaryKey:
|
||||||
class: SilverStripe\ORM\FieldType\DBPrimaryKey
|
class: SilverStripe\ORM\FieldType\DBPrimaryKey
|
||||||
Text:
|
Text:
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
namespace SilverStripe\Dev\Validation;
|
namespace SilverStripe\Dev\Validation;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
use ReflectionException;
|
use ReflectionException;
|
||||||
use SilverStripe\Core\ClassInfo;
|
use SilverStripe\Core\ClassInfo;
|
||||||
use SilverStripe\Core\Config\Configurable;
|
use SilverStripe\Core\Config\Configurable;
|
||||||
use SilverStripe\Core\Injector\Injectable;
|
use SilverStripe\Core\Injector\Injectable;
|
||||||
use SilverStripe\Core\Resettable;
|
use SilverStripe\Core\Resettable;
|
||||||
use SilverStripe\ORM\DataObject;
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\ORM\DataObjectSchema;
|
||||||
use SilverStripe\ORM\DB;
|
use SilverStripe\ORM\DB;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -291,6 +293,25 @@ class RelationValidationService implements Resettable
|
|||||||
$relations = (array) $singleton->config()->uninherited('has_one');
|
$relations = (array) $singleton->config()->uninherited('has_one');
|
||||||
|
|
||||||
foreach ($relations as $relationName => $relationData) {
|
foreach ($relations as $relationName => $relationData) {
|
||||||
|
if (is_array($relationData)) {
|
||||||
|
$spec = $relationData;
|
||||||
|
if (!isset($spec['class'])) {
|
||||||
|
$this->logError($class, $relationName, 'No class has been defined for this relation.');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$relationData = $spec['class'];
|
||||||
|
if (($spec[DataObjectSchema::HAS_ONE_MULTI_RELATIONAL] ?? false) === true
|
||||||
|
&& $relationData !== DataObject::class
|
||||||
|
) {
|
||||||
|
$this->logError(
|
||||||
|
$class,
|
||||||
|
$relationName,
|
||||||
|
'has_one relation that can handle multiple reciprocal has_many relations must be polymorphic.'
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->isIgnored($class, $relationName)) {
|
if ($this->isIgnored($class, $relationName)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -305,6 +326,11 @@ class RelationValidationService implements Resettable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip checking for back relations when has_one is polymorphic
|
||||||
|
if ($relationData === DataObject::class) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!is_subclass_of($relationData, DataObject::class)) {
|
if (!is_subclass_of($relationData, DataObject::class)) {
|
||||||
$this->logError(
|
$this->logError(
|
||||||
$class,
|
$class,
|
||||||
@ -616,7 +642,8 @@ class RelationValidationService implements Resettable
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $throughRelations[$to];
|
$spec = $throughRelations[$to];
|
||||||
|
return is_array($spec) ? $spec['class'] ?? null : $spec;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $relationData;
|
return $relationData;
|
||||||
|
@ -204,6 +204,14 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
|||||||
$classKey = $list->getForeignClassKey();
|
$classKey = $list->getForeignClassKey();
|
||||||
$class = $list->getForeignClass();
|
$class = $list->getForeignClass();
|
||||||
$this->record->$classKey = $class;
|
$this->record->$classKey = $class;
|
||||||
|
|
||||||
|
// If the has_one relation storing the data can handle multiple reciprocal has_many relations,
|
||||||
|
// make sure we tell it which has_many relation this belongs to.
|
||||||
|
$relation = $list->getForeignRelation();
|
||||||
|
if ($relation) {
|
||||||
|
$relationKey = $list->getForeignRelationKey();
|
||||||
|
$this->record->$relationKey = $relation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ use SilverStripe\Core\Config\Config;
|
|||||||
use SilverStripe\Core\Injector\Injector;
|
use SilverStripe\Core\Injector\Injector;
|
||||||
use SilverStripe\Core\Resettable;
|
use SilverStripe\Core\Resettable;
|
||||||
use SilverStripe\Dev\Debug;
|
use SilverStripe\Dev\Debug;
|
||||||
|
use SilverStripe\Dev\Deprecation;
|
||||||
use SilverStripe\Forms\FieldList;
|
use SilverStripe\Forms\FieldList;
|
||||||
use SilverStripe\Forms\FormField;
|
use SilverStripe\Forms\FormField;
|
||||||
use SilverStripe\Forms\FormScaffolder;
|
use SilverStripe\Forms\FormScaffolder;
|
||||||
@ -1972,12 +1973,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine type and nature of foreign relation
|
// Determine type and nature of foreign relation
|
||||||
$joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
|
$details = $schema->getHasManyComponentDetails(static::class, $componentName);
|
||||||
/** @var HasManyList $result */
|
if ($details['polymorphic']) {
|
||||||
if ($polymorphic) {
|
$result = PolymorphicHasManyList::create($componentClass, $details['joinField'], static::class);
|
||||||
$result = PolymorphicHasManyList::create($componentClass, $joinField, static::class);
|
if ($details['needsRelation']) {
|
||||||
|
Deprecation::withNoReplacement(fn () => $result->setForeignRelation($componentName));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$result = HasManyList::create($componentClass, $joinField);
|
$result = HasManyList::create($componentClass, $details['joinField']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result
|
return $result
|
||||||
@ -1993,16 +1996,21 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
*/
|
*/
|
||||||
public function getRelationClass($relationName)
|
public function getRelationClass($relationName)
|
||||||
{
|
{
|
||||||
// Parse many_many
|
// Parse many_many, which can have an array instead of a class name
|
||||||
$manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
|
$manyManyComponent = static::getSchema()->manyManyComponent(static::class, $relationName);
|
||||||
if ($manyManyComponent) {
|
if ($manyManyComponent) {
|
||||||
return $manyManyComponent['childClass'];
|
return $manyManyComponent['childClass'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go through all relationship configuration fields.
|
// Parse has_one, which can have an array instead of a class name
|
||||||
|
$hasOneComponent = static::getSchema()->hasOneComponent(static::class, $relationName);
|
||||||
|
if ($hasOneComponent) {
|
||||||
|
return $hasOneComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go through all remaining relationship configuration fields.
|
||||||
$config = $this->config();
|
$config = $this->config();
|
||||||
$candidates = array_merge(
|
$candidates = array_merge(
|
||||||
($relations = $config->get('has_one')) ? $relations : [],
|
|
||||||
($relations = $config->get('has_many')) ? $relations : [],
|
($relations = $config->get('has_many')) ? $relations : [],
|
||||||
($relations = $config->get('belongs_to')) ? $relations : []
|
($relations = $config->get('belongs_to')) ? $relations : []
|
||||||
);
|
);
|
||||||
@ -2228,15 +2236,20 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the class of a one-to-one component. If $component is null, return all of the one-to-one components and
|
* Return the class of a all has_one relations.
|
||||||
* their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
|
|
||||||
*
|
*
|
||||||
* @return string|array The class of the one-to-one component, or an array of all one-to-one components and
|
* @return array An array of all has_one components and their classes.
|
||||||
* their classes.
|
|
||||||
*/
|
*/
|
||||||
public function hasOne()
|
public function hasOne()
|
||||||
{
|
{
|
||||||
return (array)$this->config()->get('has_one');
|
$hasOne = (array) $this->config()->get('has_one');
|
||||||
|
// Boil down has_one spec to just the class name
|
||||||
|
foreach ($hasOne as $relationName => $spec) {
|
||||||
|
if (is_array($spec)) {
|
||||||
|
$hasOne[$relationName] = DataObject::getSchema()->hasOneComponent(static::class, $relationName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $hasOne;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,6 +24,11 @@ class DataObjectSchema
|
|||||||
use Injectable;
|
use Injectable;
|
||||||
use Configurable;
|
use Configurable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration key for has_one relations that can support multiple reciprocal has_many relations.
|
||||||
|
*/
|
||||||
|
public const HAS_ONE_MULTI_RELATIONAL = 'multirelational';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default separate for table namespaces. Can be set to any string for
|
* Default separate for table namespaces. Can be set to any string for
|
||||||
* databases that do not support some characters.
|
* databases that do not support some characters.
|
||||||
@ -501,7 +506,20 @@ class DataObjectSchema
|
|||||||
|
|
||||||
// Add in all has_ones
|
// Add in all has_ones
|
||||||
$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED) ?: [];
|
$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED) ?: [];
|
||||||
foreach ($hasOne as $fieldName => $hasOneClass) {
|
foreach ($hasOne as $fieldName => $spec) {
|
||||||
|
if (is_array($spec)) {
|
||||||
|
if (!isset($spec['class'])) {
|
||||||
|
throw new LogicException("has_one relation {$class}.{$fieldName} must declare a class");
|
||||||
|
}
|
||||||
|
// Handle has_one which handles multiple reciprocal has_many relations
|
||||||
|
$hasOneClass = $spec['class'];
|
||||||
|
if (($spec[self::HAS_ONE_MULTI_RELATIONAL] ?? false) === true) {
|
||||||
|
$compositeFields[$fieldName] = 'PolymorphicRelationAwareForeignKey';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$hasOneClass = $spec;
|
||||||
|
}
|
||||||
if ($hasOneClass === DataObject::class) {
|
if ($hasOneClass === DataObject::class) {
|
||||||
$compositeFields[$fieldName] = 'PolymorphicForeignKey';
|
$compositeFields[$fieldName] = 'PolymorphicForeignKey';
|
||||||
} else {
|
} else {
|
||||||
@ -902,12 +920,34 @@ class DataObjectSchema
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$spec = $hasOnes[$component];
|
||||||
|
|
||||||
// Validate
|
// Validate
|
||||||
$relationClass = $hasOnes[$component];
|
if (is_array($spec)) {
|
||||||
|
$this->checkHasOneArraySpec($class, $component, $spec);
|
||||||
|
}
|
||||||
|
$relationClass = is_array($spec) ? $spec['class'] : $spec;
|
||||||
$this->checkRelationClass($class, $component, $relationClass, 'has_one');
|
$this->checkRelationClass($class, $component, $relationClass, 'has_one');
|
||||||
|
|
||||||
return $relationClass;
|
return $relationClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a has_one relation handles multiple reciprocal has_many relations.
|
||||||
|
*
|
||||||
|
* @return bool True if the relation exists and handles multiple reciprocal has_many relations.
|
||||||
|
*/
|
||||||
|
public function hasOneComponentHandlesMultipleRelations(string $class, string $component): bool
|
||||||
|
{
|
||||||
|
$hasOnes = Config::forClass($class)->get('has_one');
|
||||||
|
if (!isset($hasOnes[$component])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$spec = $hasOnes[$component];
|
||||||
|
return ($spec[self::HAS_ONE_MULTI_RELATIONAL] ?? false) === true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return data for a specific belongs_to component.
|
* Return data for a specific belongs_to component.
|
||||||
*
|
*
|
||||||
@ -1047,6 +1087,20 @@ class DataObjectSchema
|
|||||||
*/
|
*/
|
||||||
public function getRemoteJoinField($class, $component, $type = 'has_many', &$polymorphic = false)
|
public function getRemoteJoinField($class, $component, $type = 'has_many', &$polymorphic = false)
|
||||||
{
|
{
|
||||||
|
return $this->getBelongsToAndHasManyDetails($class, $component, $type, $polymorphic)['joinField'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHasManyComponentDetails(string $class, string $component): array
|
||||||
|
{
|
||||||
|
return $this->getBelongsToAndHasManyDetails($class, $component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getBelongsToAndHasManyDetails(
|
||||||
|
string $class,
|
||||||
|
string $component,
|
||||||
|
string $type = 'has_many',
|
||||||
|
&$polymorphic = false
|
||||||
|
): array {
|
||||||
// Extract relation from current object
|
// Extract relation from current object
|
||||||
if ($type === 'has_many') {
|
if ($type === 'has_many') {
|
||||||
$remoteClass = $this->hasManyComponent($class, $component, false);
|
$remoteClass = $this->hasManyComponent($class, $component, false);
|
||||||
@ -1071,6 +1125,11 @@ class DataObjectSchema
|
|||||||
|
|
||||||
// Reference remote has_one to check against
|
// Reference remote has_one to check against
|
||||||
$remoteRelations = Config::inst()->get($remoteClass, 'has_one');
|
$remoteRelations = Config::inst()->get($remoteClass, 'has_one');
|
||||||
|
foreach ($remoteRelations as $key => $value) {
|
||||||
|
if (is_array($value)) {
|
||||||
|
$remoteRelations[$key] = $this->hasOneComponent($remoteClass, $key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Without an explicit field name, attempt to match the first remote field
|
// Without an explicit field name, attempt to match the first remote field
|
||||||
// with the same type as the current class
|
// with the same type as the current class
|
||||||
@ -1104,14 +1163,23 @@ class DataObjectSchema
|
|||||||
on class {$class}");
|
on class {$class}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inspect resulting found relation
|
$polymorphic = $this->hasOneComponent($remoteClass, $remoteField) === DataObject::class;
|
||||||
if ($remoteRelations[$remoteField] === DataObject::class) {
|
$remoteClassField = $polymorphic ? $remoteField . 'Class' : null;
|
||||||
$polymorphic = true;
|
$needsRelation = $type === 'has_many' && $polymorphic && $this->hasOneComponentHandlesMultipleRelations($remoteClass, $remoteField);
|
||||||
return $remoteField; // Composite polymorphic field does not include 'ID' suffix
|
$remoteRelationField = $needsRelation ? $remoteField . 'Relation' : null;
|
||||||
} else {
|
|
||||||
$polymorphic = false;
|
// This must be after the above assignments, as they rely on the original value.
|
||||||
return $remoteField . 'ID';
|
if (!$polymorphic) {
|
||||||
|
$remoteField .= 'ID';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'joinField' => $remoteField,
|
||||||
|
'relationField' => $remoteRelationField,
|
||||||
|
'classField' => $remoteClassField,
|
||||||
|
'polymorphic' => $polymorphic,
|
||||||
|
'needsRelation' => $needsRelation,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1204,6 +1272,24 @@ class DataObjectSchema
|
|||||||
return $joinClass;
|
return $joinClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function checkHasOneArraySpec(string $class, string $component, array $spec): void
|
||||||
|
{
|
||||||
|
if (!array_key_exists('class', $spec)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"has_one relation {$class}.{$component} doesn't define a class for the relation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($spec[self::HAS_ONE_MULTI_RELATIONAL] ?? false) === true
|
||||||
|
&& $spec['class'] !== DataObject::class
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"has_one relation {$class}.{$component} must be polymorphic, or not support multiple"
|
||||||
|
. 'reciprocal has_many relations'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a given class is valid for a relation
|
* Validate a given class is valid for a relation
|
||||||
*
|
*
|
||||||
|
@ -1025,8 +1025,6 @@ class DataQuery
|
|||||||
* Join the given has_many relation to this query.
|
* Join the given has_many relation to this query.
|
||||||
* Also works with belongs_to
|
* Also works with belongs_to
|
||||||
*
|
*
|
||||||
* Doesn't work with polymorphic relationships
|
|
||||||
*
|
|
||||||
* @param string $localClass Name of class that has the has_many to the joined class
|
* @param string $localClass Name of class that has the has_many to the joined class
|
||||||
* @param string $localField Name of the has_many relationship to join
|
* @param string $localField Name of the has_many relationship to join
|
||||||
* @param string $foreignClass Class to join
|
* @param string $foreignClass Class to join
|
||||||
@ -1065,6 +1063,15 @@ class DataQuery
|
|||||||
$localClassColumn = $schema->sqlColumnForField($localClass, 'ClassName', $localPrefix);
|
$localClassColumn = $schema->sqlColumnForField($localClass, 'ClassName', $localPrefix);
|
||||||
$joinExpression =
|
$joinExpression =
|
||||||
"{$foreignKeyIDColumn} = {$localIDColumn} AND {$foreignKeyClassColumn} = {$localClassColumn}";
|
"{$foreignKeyIDColumn} = {$localIDColumn} AND {$foreignKeyClassColumn} = {$localClassColumn}";
|
||||||
|
|
||||||
|
// Add relation key if the has_many points to a has_one that could handle multiple reciprocal has_many relations
|
||||||
|
if ($type === 'has_many') {
|
||||||
|
$details = $schema->getHasManyComponentDetails($localClass, $localField);
|
||||||
|
if ($details['needsRelation']) {
|
||||||
|
$foreignKeyRelationColumn = $schema->sqlColumnForField($foreignClass, "{$foreignKey}Relation", $foreignPrefix);
|
||||||
|
$joinExpression .= " AND {$foreignKeyRelationColumn} = {$localField}";
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$foreignKeyIDColumn = $schema->sqlColumnForField($foreignClass, $foreignKey, $foreignPrefix);
|
$foreignKeyIDColumn = $schema->sqlColumnForField($foreignClass, $foreignKey, $foreignPrefix);
|
||||||
$joinExpression = "{$foreignKeyIDColumn} = {$localIDColumn}";
|
$joinExpression = "{$foreignKeyIDColumn} = {$localIDColumn}";
|
||||||
|
38
src/ORM/FieldType/DBPolymorphicRelationAwareForeignKey.php
Normal file
38
src/ORM/FieldType/DBPolymorphicRelationAwareForeignKey.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\FieldType;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A special polymorphic ForeignKey class that allows a single has_one relation to map to multiple has_many relations
|
||||||
|
*/
|
||||||
|
class DBPolymorphicRelationAwareForeignKey extends DBPolymorphicForeignKey
|
||||||
|
{
|
||||||
|
private static array $composite_db = [
|
||||||
|
'Relation' => 'Varchar',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value of the "Relation" this key points to
|
||||||
|
*
|
||||||
|
* @return string Name of the has_many relation being stored
|
||||||
|
*/
|
||||||
|
public function getRelationValue(): string
|
||||||
|
{
|
||||||
|
return $this->getField('Relation');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the value of the "Relation" this key points to
|
||||||
|
*
|
||||||
|
* @param string $value Name of the has_many relation being stored
|
||||||
|
* @param bool $markChanged Mark this field as changed?
|
||||||
|
*/
|
||||||
|
public function setRelationValue(string $value, bool $markChanged = true): static
|
||||||
|
{
|
||||||
|
$this->setField('Relation', $value, $markChanged);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
@ -5,9 +5,11 @@ namespace SilverStripe\ORM;
|
|||||||
use SilverStripe\Core\ClassInfo;
|
use SilverStripe\Core\ClassInfo;
|
||||||
use SilverStripe\Core\Convert;
|
use SilverStripe\Core\Convert;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
use SilverStripe\Dev\Deprecation;
|
||||||
|
use Traversable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a has_many list linked against a polymorphic relationship
|
* Represents a has_many list linked against a polymorphic relationship.
|
||||||
*/
|
*/
|
||||||
class PolymorphicHasManyList extends HasManyList
|
class PolymorphicHasManyList extends HasManyList
|
||||||
{
|
{
|
||||||
@ -20,7 +22,13 @@ class PolymorphicHasManyList extends HasManyList
|
|||||||
protected $classForeignKey;
|
protected $classForeignKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the name of the class this relation is filtered by
|
* Name of the foreign key field that references the relation name, for has_one
|
||||||
|
* relations that can handle multiple reciprocal has_many relations.
|
||||||
|
*/
|
||||||
|
protected string $relationForeignKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the name of the class this (has_many) relation is filtered by
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
@ -30,19 +38,51 @@ class PolymorphicHasManyList extends HasManyList
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the field name which holds the related object class.
|
* Retrieve the name of the has_many relation this list is filtered by
|
||||||
|
*/
|
||||||
|
public function getForeignRelation(): ?string
|
||||||
|
{
|
||||||
|
return $this->dataQuery->getQueryParam('Foreign.Relation');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the name of the has_many relation this list is filtered by
|
||||||
|
*
|
||||||
|
* @deprecated 5.2.0 Will be replaced with a parameter in the constructor
|
||||||
|
*/
|
||||||
|
public function setForeignRelation(string $relationName): static
|
||||||
|
{
|
||||||
|
Deprecation::notice('5.2.0', 'Will be replaced with a parameter in the constructor');
|
||||||
|
$this->dataQuery->where(["\"{$this->relationForeignKey}\"" => $relationName]);
|
||||||
|
$this->dataQuery->setQueryParam('Foreign.Relation', $relationName);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the field name which holds the related (has_many) object class.
|
||||||
*/
|
*/
|
||||||
public function getForeignClassKey(): string
|
public function getForeignClassKey(): string
|
||||||
{
|
{
|
||||||
return $this->classForeignKey;
|
return $this->classForeignKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the field name which holds the has_many relation name.
|
||||||
|
*
|
||||||
|
* Note that this will return a value even if the has_one relation
|
||||||
|
* doesn't support multiple reciprocal has_many relations.
|
||||||
|
*/
|
||||||
|
public function getForeignRelationKey(): string
|
||||||
|
{
|
||||||
|
return $this->relationForeignKey;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new PolymorphicHasManyList relation list.
|
* Create a new PolymorphicHasManyList relation list.
|
||||||
*
|
*
|
||||||
* @param string $dataClass The class of the DataObjects that this will list.
|
* @param string $dataClass The class of the DataObjects that this will list.
|
||||||
* @param string $foreignField The name of the composite foreign relation field. Used
|
* @param string $foreignField The name of the composite foreign (has_one) relation field. Used
|
||||||
* to generate the ID and Class foreign keys.
|
* to generate the ID, Class, and Relation foreign keys.
|
||||||
* @param string $foreignClass Name of the class filter this relation is filtered against
|
* @param string $foreignClass Name of the class filter this relation is filtered against
|
||||||
*/
|
*/
|
||||||
public function __construct($dataClass, $foreignField, $foreignClass)
|
public function __construct($dataClass, $foreignField, $foreignClass)
|
||||||
@ -50,6 +90,7 @@ class PolymorphicHasManyList extends HasManyList
|
|||||||
// Set both id foreign key (as in HasManyList) and the class foreign key
|
// Set both id foreign key (as in HasManyList) and the class foreign key
|
||||||
parent::__construct($dataClass, "{$foreignField}ID");
|
parent::__construct($dataClass, "{$foreignField}ID");
|
||||||
$this->classForeignKey = "{$foreignField}Class";
|
$this->classForeignKey = "{$foreignField}Class";
|
||||||
|
$this->relationForeignKey = "{$foreignField}Relation";
|
||||||
|
|
||||||
// Ensure underlying DataQuery globally references the class filter
|
// Ensure underlying DataQuery globally references the class filter
|
||||||
$this->dataQuery->setQueryParam('Foreign.Class', $foreignClass);
|
$this->dataQuery->setQueryParam('Foreign.Class', $foreignClass);
|
||||||
@ -98,11 +139,19 @@ class PolymorphicHasManyList extends HasManyList
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set the {$relationName}Class field value
|
||||||
$foreignKey = $this->foreignKey;
|
$foreignKey = $this->foreignKey;
|
||||||
$classForeignKey = $this->classForeignKey;
|
$classForeignKey = $this->classForeignKey;
|
||||||
$item->$foreignKey = $foreignID;
|
$item->$foreignKey = $foreignID;
|
||||||
$item->$classForeignKey = $this->getForeignClass();
|
$item->$classForeignKey = $this->getForeignClass();
|
||||||
|
|
||||||
|
// set the {$relationName}Relation field value if appropriate
|
||||||
|
$foreignRelation = $this->getForeignRelation();
|
||||||
|
if ($foreignRelation) {
|
||||||
|
$relationForeignKey = $this->getForeignRelationKey();
|
||||||
|
$item->$relationForeignKey = $foreignRelation;
|
||||||
|
}
|
||||||
|
|
||||||
$item->write();
|
$item->write();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,6 +178,13 @@ class PolymorphicHasManyList extends HasManyList
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't remove item with unrelated relation key
|
||||||
|
$foreignRelation = $this->getForeignRelation();
|
||||||
|
$relationForeignKey = $this->getForeignRelationKey();
|
||||||
|
if (!$this->relationMatches($item->$relationForeignKey, $foreignRelation)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Don't remove item which doesn't belong to this list
|
// Don't remove item which doesn't belong to this list
|
||||||
$foreignID = $this->getForeignID();
|
$foreignID = $this->getForeignID();
|
||||||
$foreignKey = $this->foreignKey;
|
$foreignKey = $this->foreignKey;
|
||||||
@ -137,9 +193,20 @@ class PolymorphicHasManyList extends HasManyList
|
|||||||
|| $foreignID == $item->$foreignKey
|
|| $foreignID == $item->$foreignKey
|
||||||
|| (is_array($foreignID) && in_array($item->$foreignKey, $foreignID ?? []))
|
|| (is_array($foreignID) && in_array($item->$foreignKey, $foreignID ?? []))
|
||||||
) {
|
) {
|
||||||
|
// Unset the foreign relation key if appropriate
|
||||||
|
if ($foreignRelation) {
|
||||||
|
$item->$relationForeignKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unset the rest of the relation and write the record
|
||||||
$item->$foreignKey = null;
|
$item->$foreignKey = null;
|
||||||
$item->$classForeignKey = null;
|
$item->$classForeignKey = null;
|
||||||
$item->write();
|
$item->write();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function relationMatches(?string $actual, ?string $expected): bool
|
||||||
|
{
|
||||||
|
return (empty($actual) && empty($expected)) || $actual === $expected;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,8 @@ class Member extends DataObject implements TestOnly
|
|||||||
*/
|
*/
|
||||||
private static $has_many = [
|
private static $has_many = [
|
||||||
'TemporaryMembers' => Freelancer::class . '.TemporaryMember',
|
'TemporaryMembers' => Freelancer::class . '.TemporaryMember',
|
||||||
|
'ManyTeams' => Team::class . '.SingleMember',
|
||||||
|
'ManyMoreTeams' => Team::class . '.SingleMember',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -6,6 +6,8 @@ use Page;
|
|||||||
use SilverStripe\Core\Config\Config;
|
use SilverStripe\Core\Config\Config;
|
||||||
use SilverStripe\Dev\SapphireTest;
|
use SilverStripe\Dev\SapphireTest;
|
||||||
use SilverStripe\Dev\Validation\RelationValidationService;
|
use SilverStripe\Dev\Validation\RelationValidationService;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\ORM\DataObjectSchema;
|
||||||
|
|
||||||
class RelationValidationTest extends SapphireTest
|
class RelationValidationTest extends SapphireTest
|
||||||
{
|
{
|
||||||
@ -107,6 +109,14 @@ class RelationValidationTest extends SapphireTest
|
|||||||
'SilverStripe\Dev\Tests\Validation\Member / Hat : Back relation is ambiguous',
|
'SilverStripe\Dev\Tests\Validation\Member / Hat : Back relation is ambiguous',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'polymorphic has one' => [
|
||||||
|
Team::class,
|
||||||
|
'has_one',
|
||||||
|
[
|
||||||
|
'SingleMember' => DataObject::class,
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
],
|
||||||
'invalid has one' => [
|
'invalid has one' => [
|
||||||
Member::class,
|
Member::class,
|
||||||
'has_one',
|
'has_one',
|
||||||
@ -118,6 +128,45 @@ class RelationValidationTest extends SapphireTest
|
|||||||
'SilverStripe\Dev\Tests\Validation\Member / HomeTeam : Relation SilverStripe\Dev\Tests\Validation\Team.UnnecessaryRelation is not in the expected format (needs class only format).'
|
'SilverStripe\Dev\Tests\Validation\Member / HomeTeam : Relation SilverStripe\Dev\Tests\Validation\Team.UnnecessaryRelation is not in the expected format (needs class only format).'
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'has_one missing class in array config' => [
|
||||||
|
Team::class,
|
||||||
|
'has_one',
|
||||||
|
[
|
||||||
|
'SingleMember' => [
|
||||||
|
DataObjectSchema::HAS_ONE_MULTI_RELATIONAL => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'SilverStripe\Dev\Tests\Validation\Team / SingleMember : No class has been defined for this relation.'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'multi-relational has_one should be polymorphic' => [
|
||||||
|
Team::class,
|
||||||
|
'has_one',
|
||||||
|
[
|
||||||
|
'SingleMember' => [
|
||||||
|
'class' => Member::class,
|
||||||
|
DataObjectSchema::HAS_ONE_MULTI_RELATIONAL => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'SilverStripe\Dev\Tests\Validation\Team / SingleMember : has_one relation that can handle multiple reciprocal has_many relations must be polymorphic.'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'has_one defines class in array config' => [
|
||||||
|
Team::class,
|
||||||
|
'has_one',
|
||||||
|
[
|
||||||
|
'SingleMember' => [
|
||||||
|
'class' => Member::class,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
// Note there's no message about the has_one class, which is technically correctly defined.
|
||||||
|
// The bad thing now is just that we still have multiple has_many relations pointing at it.
|
||||||
|
[
|
||||||
|
'SilverStripe\Dev\Tests\Validation\Team / SingleMember : Back relation is ambiguous'
|
||||||
|
],
|
||||||
|
],
|
||||||
'ambiguous has_many - no relation name' => [
|
'ambiguous has_many - no relation name' => [
|
||||||
Team::class,
|
Team::class,
|
||||||
'has_many',
|
'has_many',
|
||||||
|
@ -4,6 +4,7 @@ namespace SilverStripe\Dev\Tests\Validation;
|
|||||||
|
|
||||||
use SilverStripe\Dev\TestOnly;
|
use SilverStripe\Dev\TestOnly;
|
||||||
use SilverStripe\ORM\DataObject;
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\ORM\DataObjectSchema;
|
||||||
use SilverStripe\ORM\HasManyList;
|
use SilverStripe\ORM\HasManyList;
|
||||||
use SilverStripe\ORM\ManyManyList;
|
use SilverStripe\ORM\ManyManyList;
|
||||||
use SilverStripe\ORM\ManyManyThroughList;
|
use SilverStripe\ORM\ManyManyThroughList;
|
||||||
@ -31,6 +32,13 @@ class Team extends DataObject implements TestOnly
|
|||||||
'Title' => 'Varchar(255)',
|
'Title' => 'Varchar(255)',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private static array $has_one = [
|
||||||
|
'SingleMember' => [
|
||||||
|
'class' => DataObject::class,
|
||||||
|
DataObjectSchema::HAS_ONE_MULTI_RELATIONAL => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
|
@ -13,6 +13,7 @@ use SilverStripe\Forms\HiddenField;
|
|||||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\Category;
|
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\Category;
|
||||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\CategoryController;
|
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\CategoryController;
|
||||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\GroupController;
|
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\GroupController;
|
||||||
|
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\MultiRelationalPeopleGroup;
|
||||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\PeopleGroup;
|
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\PeopleGroup;
|
||||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\Person;
|
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\Person;
|
||||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\PolymorphicPeopleGroup;
|
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\PolymorphicPeopleGroup;
|
||||||
@ -26,6 +27,7 @@ class GridFieldDetailFormTest extends FunctionalTest
|
|||||||
Person::class,
|
Person::class,
|
||||||
PeopleGroup::class,
|
PeopleGroup::class,
|
||||||
PolymorphicPeopleGroup::class,
|
PolymorphicPeopleGroup::class,
|
||||||
|
MultiRelationalPeopleGroup::class,
|
||||||
Category::class,
|
Category::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -143,6 +145,31 @@ class GridFieldDetailFormTest extends FunctionalTest
|
|||||||
$this->assertEquals($group->ID, $record->PolymorphicGroupID);
|
$this->assertEquals($group->ID, $record->PolymorphicGroupID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testAddFormWithMultiRelationalHasOne(): void
|
||||||
|
{
|
||||||
|
// Log in for permissions check
|
||||||
|
$this->logInWithPermission('ADMIN');
|
||||||
|
// Prepare gridfield and other objects
|
||||||
|
$group = new MultiRelationalPeopleGroup();
|
||||||
|
$group->write();
|
||||||
|
$gridField = $group->getCMSFields()->dataFieldByName('People');
|
||||||
|
$gridField->setForm(new Form());
|
||||||
|
$detailForm = $gridField->getConfig()->getComponentByType(GridFieldDetailForm::class);
|
||||||
|
$record = new Person();
|
||||||
|
|
||||||
|
// Trigger creation of the item edit form
|
||||||
|
$reflectionDetailForm = new \ReflectionClass($detailForm);
|
||||||
|
$reflectionMethod = $reflectionDetailForm->getMethod('getItemRequestHandler');
|
||||||
|
$reflectionMethod->setAccessible(true);
|
||||||
|
$itemrequest = $reflectionMethod->invoke($detailForm, $gridField, $record, new Controller());
|
||||||
|
$itemrequest->ItemEditForm();
|
||||||
|
|
||||||
|
// The polymorphic and multi-relational values should be pre-loaded
|
||||||
|
$this->assertEquals(MultiRelationalPeopleGroup::class, $record->MultiRelationalGroupClass);
|
||||||
|
$this->assertEquals('People', $record->MultiRelationalGroupRelation);
|
||||||
|
$this->assertEquals($group->ID, $record->MultiRelationalGroupID);
|
||||||
|
}
|
||||||
|
|
||||||
public function testViewForm()
|
public function testViewForm()
|
||||||
{
|
{
|
||||||
$this->logInWithPermission('ADMIN');
|
$this->logInWithPermission('ADMIN');
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
use SilverStripe\Forms\GridField\GridField;
|
||||||
|
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
|
||||||
|
class MultiRelationalPeopleGroup extends DataObject implements TestOnly
|
||||||
|
{
|
||||||
|
private static $table_name = 'GridFieldDetailFormTest_MultiRelationalPeopleGroup';
|
||||||
|
|
||||||
|
private static $db = [
|
||||||
|
'Name' => 'Varchar'
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $has_many = [
|
||||||
|
'People' => Person::class . '.MultiRelationalGroup'
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $default_sort = '"Name"';
|
||||||
|
|
||||||
|
public function getCMSFields()
|
||||||
|
{
|
||||||
|
$fields = parent::getCMSFields();
|
||||||
|
$fields->replaceField(
|
||||||
|
'People',
|
||||||
|
GridField::create(
|
||||||
|
'People',
|
||||||
|
'People',
|
||||||
|
$this->People(),
|
||||||
|
GridFieldConfig_RelationEditor::create()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ use SilverStripe\Forms\GridField\GridField;
|
|||||||
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
|
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
|
||||||
use SilverStripe\Forms\RequiredFields;
|
use SilverStripe\Forms\RequiredFields;
|
||||||
use SilverStripe\ORM\DataObject;
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\ORM\DataObjectSchema;
|
||||||
|
|
||||||
class Person extends DataObject implements TestOnly
|
class Person extends DataObject implements TestOnly
|
||||||
{
|
{
|
||||||
@ -20,6 +21,10 @@ class Person extends DataObject implements TestOnly
|
|||||||
private static $has_one = [
|
private static $has_one = [
|
||||||
'Group' => PeopleGroup::class,
|
'Group' => PeopleGroup::class,
|
||||||
'PolymorphicGroup' => DataObject::class,
|
'PolymorphicGroup' => DataObject::class,
|
||||||
|
'MultiRelationalGroup' => [
|
||||||
|
'class' => DataObject::class,
|
||||||
|
DataObjectSchema::HAS_ONE_MULTI_RELATIONAL => true,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
private static $many_many = [
|
private static $many_many = [
|
||||||
|
@ -16,7 +16,7 @@ class PolymorphicPeopleGroup extends DataObject implements TestOnly
|
|||||||
];
|
];
|
||||||
|
|
||||||
private static $has_many = [
|
private static $has_many = [
|
||||||
'People' => Person::class
|
'People' => Person::class . '.PolymorphicGroup'
|
||||||
];
|
];
|
||||||
|
|
||||||
private static $default_sort = '"Name"';
|
private static $default_sort = '"Name"';
|
||||||
|
@ -159,68 +159,94 @@ class DataObjectSchemaTest extends SapphireTest
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFieldSpec()
|
/**
|
||||||
|
* @dataProvider provideFieldSpec
|
||||||
|
*/
|
||||||
|
public function testFieldSpec(array $args, array $expected): void
|
||||||
{
|
{
|
||||||
$schema = DataObject::getSchema();
|
$schema = DataObject::getSchema();
|
||||||
$this->assertEquals(
|
$this->assertEquals($expected, $schema->fieldSpecs(...$args));
|
||||||
[
|
}
|
||||||
'ID' => 'PrimaryKey',
|
|
||||||
'ClassName' => 'DBClassName',
|
|
||||||
'LastEdited' => 'DBDatetime',
|
|
||||||
'Created' => 'DBDatetime',
|
|
||||||
'Title' => 'Varchar',
|
|
||||||
'Description' => 'Varchar',
|
|
||||||
'MoneyFieldCurrency' => 'Varchar(3)',
|
|
||||||
'MoneyFieldAmount' => 'Decimal(19,4)',
|
|
||||||
'MoneyField' => 'Money',
|
|
||||||
],
|
|
||||||
$schema->fieldSpecs(HasFields::class)
|
|
||||||
);
|
|
||||||
$this->assertEquals(
|
|
||||||
[
|
|
||||||
'ID' => DataObjectSchemaTest\HasFields::class . '.PrimaryKey',
|
|
||||||
'ClassName' => DataObjectSchemaTest\BaseDataClass::class . '.DBClassName',
|
|
||||||
'LastEdited' => DataObjectSchemaTest\BaseDataClass::class . '.DBDatetime',
|
|
||||||
'Created' => DataObjectSchemaTest\BaseDataClass::class . '.DBDatetime',
|
|
||||||
'Title' => DataObjectSchemaTest\BaseDataClass::class . '.Varchar',
|
|
||||||
'Description' => DataObjectSchemaTest\HasFields::class . '.Varchar',
|
|
||||||
'MoneyFieldCurrency' => DataObjectSchemaTest\HasFields::class . '.Varchar(3)',
|
|
||||||
'MoneyFieldAmount' => DataObjectSchemaTest\HasFields::class . '.Decimal(19,4)',
|
|
||||||
'MoneyField' => DataObjectSchemaTest\HasFields::class . '.Money',
|
|
||||||
],
|
|
||||||
$schema->fieldSpecs(HasFields::class, DataObjectSchema::INCLUDE_CLASS)
|
|
||||||
);
|
|
||||||
// DB_ONLY excludes composite field MoneyField
|
|
||||||
$this->assertEquals(
|
|
||||||
[
|
|
||||||
'ID' => DataObjectSchemaTest\HasFields::class . '.PrimaryKey',
|
|
||||||
'ClassName' => DataObjectSchemaTest\BaseDataClass::class . '.DBClassName',
|
|
||||||
'LastEdited' => DataObjectSchemaTest\BaseDataClass::class . '.DBDatetime',
|
|
||||||
'Created' => DataObjectSchemaTest\BaseDataClass::class . '.DBDatetime',
|
|
||||||
'Title' => DataObjectSchemaTest\BaseDataClass::class . '.Varchar',
|
|
||||||
'Description' => DataObjectSchemaTest\HasFields::class . '.Varchar',
|
|
||||||
'MoneyFieldCurrency' => DataObjectSchemaTest\HasFields::class . '.Varchar(3)',
|
|
||||||
'MoneyFieldAmount' => DataObjectSchemaTest\HasFields::class . '.Decimal(19,4)'
|
|
||||||
],
|
|
||||||
$schema->fieldSpecs(
|
|
||||||
HasFields::class,
|
|
||||||
DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Use all options at once
|
public function provideFieldSpec(): array
|
||||||
$this->assertEquals(
|
{
|
||||||
[
|
return [
|
||||||
'ID' => DataObjectSchemaTest\HasFields::class . '.PrimaryKey',
|
'just pass a class' => [
|
||||||
'Description' => DataObjectSchemaTest\HasFields::class . '.Varchar',
|
'args' => [HasFields::class],
|
||||||
'MoneyFieldCurrency' => DataObjectSchemaTest\HasFields::class . '.Varchar(3)',
|
'expected' => [
|
||||||
'MoneyFieldAmount' => DataObjectSchemaTest\HasFields::class . '.Decimal(19,4)',
|
'ID' => 'PrimaryKey',
|
||||||
|
'ClassName' => 'DBClassName',
|
||||||
|
'LastEdited' => 'DBDatetime',
|
||||||
|
'Created' => 'DBDatetime',
|
||||||
|
'Title' => 'Varchar',
|
||||||
|
'Description' => 'Varchar',
|
||||||
|
'MoneyFieldCurrency' => 'Varchar(3)',
|
||||||
|
'MoneyFieldAmount' => 'Decimal(19,4)',
|
||||||
|
'MoneyField' => 'Money',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
$schema->fieldSpecs(
|
'prefix with classname' => [
|
||||||
HasFields::class,
|
'args' => [HasFields::class, DataObjectSchema::INCLUDE_CLASS],
|
||||||
DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED
|
'expected' => [
|
||||||
)
|
'ID' => DataObjectSchemaTest\HasFields::class . '.PrimaryKey',
|
||||||
);
|
'ClassName' => DataObjectSchemaTest\BaseDataClass::class . '.DBClassName',
|
||||||
|
'LastEdited' => DataObjectSchemaTest\BaseDataClass::class . '.DBDatetime',
|
||||||
|
'Created' => DataObjectSchemaTest\BaseDataClass::class . '.DBDatetime',
|
||||||
|
'Title' => DataObjectSchemaTest\BaseDataClass::class . '.Varchar',
|
||||||
|
'Description' => DataObjectSchemaTest\HasFields::class . '.Varchar',
|
||||||
|
'MoneyFieldCurrency' => DataObjectSchemaTest\HasFields::class . '.Varchar(3)',
|
||||||
|
'MoneyFieldAmount' => DataObjectSchemaTest\HasFields::class . '.Decimal(19,4)',
|
||||||
|
'MoneyField' => DataObjectSchemaTest\HasFields::class . '.Money',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'DB_ONLY excludes composite field MoneyField' => [
|
||||||
|
'args' => [
|
||||||
|
HasFields::class,
|
||||||
|
DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY,
|
||||||
|
],
|
||||||
|
'expected' => [
|
||||||
|
'ID' => DataObjectSchemaTest\HasFields::class . '.PrimaryKey',
|
||||||
|
'ClassName' => DataObjectSchemaTest\BaseDataClass::class . '.DBClassName',
|
||||||
|
'LastEdited' => DataObjectSchemaTest\BaseDataClass::class . '.DBDatetime',
|
||||||
|
'Created' => DataObjectSchemaTest\BaseDataClass::class . '.DBDatetime',
|
||||||
|
'Title' => DataObjectSchemaTest\BaseDataClass::class . '.Varchar',
|
||||||
|
'Description' => DataObjectSchemaTest\HasFields::class . '.Varchar',
|
||||||
|
'MoneyFieldCurrency' => DataObjectSchemaTest\HasFields::class . '.Varchar(3)',
|
||||||
|
'MoneyFieldAmount' => DataObjectSchemaTest\HasFields::class . '.Decimal(19,4)'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'Use all options at once' => [
|
||||||
|
'args' => [
|
||||||
|
HasFields::class,
|
||||||
|
DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED
|
||||||
|
],
|
||||||
|
'expected' => [
|
||||||
|
'ID' => DataObjectSchemaTest\HasFields::class . '.PrimaryKey',
|
||||||
|
'Description' => DataObjectSchemaTest\HasFields::class . '.Varchar',
|
||||||
|
'MoneyFieldCurrency' => DataObjectSchemaTest\HasFields::class . '.Varchar(3)',
|
||||||
|
'MoneyFieldAmount' => DataObjectSchemaTest\HasFields::class . '.Decimal(19,4)',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'has_one relations are returned correctly' => [
|
||||||
|
'args' => [WithRelation::class],
|
||||||
|
'expected' => [
|
||||||
|
'ID' => 'PrimaryKey',
|
||||||
|
'ClassName' => 'DBClassName',
|
||||||
|
'LastEdited' => 'DBDatetime',
|
||||||
|
'Created' => 'DBDatetime',
|
||||||
|
'Title' => 'Varchar',
|
||||||
|
'RelationID' => 'ForeignKey',
|
||||||
|
'PolymorphicRelationID' => 'Int',
|
||||||
|
'PolymorphicRelationClass' => "DBClassName('SilverStripe\ORM\DataObject', ['index' => false])",
|
||||||
|
'MultiRelationalRelationID' => 'Int',
|
||||||
|
'MultiRelationalRelationClass' => "DBClassName('SilverStripe\ORM\DataObject', ['index' => false])",
|
||||||
|
'MultiRelationalRelationRelation' => 'Varchar',
|
||||||
|
'PolymorphicRelation' => 'PolymorphicForeignKey',
|
||||||
|
'MultiRelationalRelation' => 'PolymorphicRelationAwareForeignKey',
|
||||||
|
'ArraySyntaxRelationID' => 'ForeignKey',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -373,4 +399,76 @@ class DataObjectSchemaTest extends SapphireTest
|
|||||||
AllIndexes::get()
|
AllIndexes::get()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideHasOneComponent
|
||||||
|
*/
|
||||||
|
|
||||||
|
public function testHasOneComponent(string $class, string $component, string $expected): void
|
||||||
|
{
|
||||||
|
$this->assertSame($expected, DataObject::getSchema()->hasOneComponent($class, $component));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideHasOneComponent(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'class' => WithRelation::class,
|
||||||
|
'component' => 'Relation',
|
||||||
|
'expected' => HasFields::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'class' => WithRelation::class,
|
||||||
|
'component' => 'PolymorphicRelation',
|
||||||
|
'expected' => DataObject::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'class' => WithRelation::class,
|
||||||
|
'component' => 'ArraySyntaxRelation',
|
||||||
|
'expected' => HasFields::class,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'class' => WithRelation::class,
|
||||||
|
'component' => 'MultiRelationalRelation',
|
||||||
|
'expected' => DataObject::class,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideHasOneComponentHandlesMultipleRelations
|
||||||
|
*/
|
||||||
|
public function testHasOneComponentHandlesMultipleRelations(string $class, string $component, bool $expected): void
|
||||||
|
{
|
||||||
|
$this->assertSame(
|
||||||
|
$expected,
|
||||||
|
DataObject::getSchema()->hasOneComponentHandlesMultipleRelations($class, $component)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideHasOneComponentHandlesMultipleRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'class' => WithRelation::class,
|
||||||
|
'component' => 'Relation',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'class' => WithRelation::class,
|
||||||
|
'component' => 'PolymorphicRelation',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'class' => WithRelation::class,
|
||||||
|
'component' => 'ArraySyntaxRelation',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'class' => WithRelation::class,
|
||||||
|
'component' => 'MultiRelationalRelation',
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,22 @@
|
|||||||
|
|
||||||
namespace SilverStripe\ORM\Tests\DataObjectSchemaTest;
|
namespace SilverStripe\ORM\Tests\DataObjectSchemaTest;
|
||||||
|
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\ORM\DataObjectSchema;
|
||||||
|
|
||||||
class WithRelation extends NoFields
|
class WithRelation extends NoFields
|
||||||
{
|
{
|
||||||
private static $table_name = 'DataObjectSchemaTest_WithRelation';
|
private static $table_name = 'DataObjectSchemaTest_WithRelation';
|
||||||
|
|
||||||
private static $has_one = [
|
private static $has_one = [
|
||||||
'Relation' => HasFields::Class
|
'Relation' => HasFields::class,
|
||||||
|
'PolymorphicRelation' => DataObject::class,
|
||||||
|
'MultiRelationalRelation' => [
|
||||||
|
'class' => DataObject::class,
|
||||||
|
DataObjectSchema::HAS_ONE_MULTI_RELATIONAL => true,
|
||||||
|
],
|
||||||
|
'ArraySyntaxRelation' => [
|
||||||
|
'class' => HasFields::class,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
namespace SilverStripe\ORM\Tests\DataObjectTest;
|
namespace SilverStripe\ORM\Tests\DataObjectTest;
|
||||||
|
|
||||||
use SilverStripe\Dev\TestOnly;
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\ORM\DataObjectSchema;
|
||||||
use SilverStripe\ORM\Tests\DataObjectTest;
|
use SilverStripe\ORM\Tests\DataObjectTest;
|
||||||
use SilverStripe\Security\Member;
|
use SilverStripe\Security\Member;
|
||||||
|
|
||||||
@ -17,6 +19,10 @@ class Player extends Member implements TestOnly
|
|||||||
|
|
||||||
private static $has_one = [
|
private static $has_one = [
|
||||||
'FavouriteTeam' => DataObjectTest\Team::class,
|
'FavouriteTeam' => DataObjectTest\Team::class,
|
||||||
|
'MultiRelational' => [
|
||||||
|
'class' => DataObject::class,
|
||||||
|
DataObjectSchema::HAS_ONE_MULTI_RELATIONAL => true,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
private static $belongs_many_many = [
|
private static $belongs_many_many = [
|
||||||
|
@ -44,7 +44,10 @@ class Team extends DataObject implements TestOnly
|
|||||||
'SubTeams' => SubTeam::class,
|
'SubTeams' => SubTeam::class,
|
||||||
'Comments' => TeamComment::class,
|
'Comments' => TeamComment::class,
|
||||||
'Fans' => Fan::class . '.Favourite', // Polymorphic - Team fans
|
'Fans' => Fan::class . '.Favourite', // Polymorphic - Team fans
|
||||||
'PlayerFans' => Player::class . '.FavouriteTeam'
|
'PlayerFans' => Player::class . '.FavouriteTeam',
|
||||||
|
// multi-relational relation:
|
||||||
|
'ManyPlayers1' => Player::class . '.MultiRelational',
|
||||||
|
'ManyPlayers2' => Player::class . '.MultiRelational',
|
||||||
];
|
];
|
||||||
|
|
||||||
private static $many_many = [
|
private static $many_many = [
|
||||||
|
@ -27,6 +27,8 @@ class DataQueryTest extends SapphireTest
|
|||||||
DataQueryTest\ObjectG::class,
|
DataQueryTest\ObjectG::class,
|
||||||
DataQueryTest\ObjectH::class,
|
DataQueryTest\ObjectH::class,
|
||||||
DataQueryTest\ObjectI::class,
|
DataQueryTest\ObjectI::class,
|
||||||
|
DataQueryTest\ObjectHasMultiRelationalHasOne::class,
|
||||||
|
DataQueryTest\ObjectHasMultiRelationalHasMany::class,
|
||||||
SQLSelectTest\CteRecursiveObject::class,
|
SQLSelectTest\CteRecursiveObject::class,
|
||||||
SQLSelectTest\TestObject::class,
|
SQLSelectTest\TestObject::class,
|
||||||
SQLSelectTest\TestBase::class,
|
SQLSelectTest\TestBase::class,
|
||||||
@ -99,6 +101,33 @@ class DataQueryTest extends SapphireTest
|
|||||||
$this->assertStringContainsString('"testctwo_DataQueryTest_C"."ID" = "DataQueryTest_B"."TestCTwoID"', $dq->sql());
|
$this->assertStringContainsString('"testctwo_DataQueryTest_C"."ID" = "DataQueryTest_B"."TestCTwoID"', $dq->sql());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideApplyRelationMultiRelational
|
||||||
|
*/
|
||||||
|
public function testApplyRelationMultiRelational(string $relation): void
|
||||||
|
{
|
||||||
|
$dq = new DataQuery(DataQueryTest\ObjectHasMultiRelationalHasMany::class);
|
||||||
|
$dq->applyRelation($relation);
|
||||||
|
$joinAlias = strtolower($relation) . '_DataQueryTest_ObjectHasMultiRelationalHasOne';
|
||||||
|
$joinAliasWithQuotes = '"' . $joinAlias . '"';
|
||||||
|
$this->assertTrue($dq->query()->isJoinedTo($joinAlias));
|
||||||
|
$this->assertStringContainsString($joinAliasWithQuotes . '."MultiRelationalID" = "DataQueryTest_ObjectHasMultiRelationalHasMany"."ID"', $dq->sql());
|
||||||
|
$this->assertStringContainsString($joinAliasWithQuotes . '."MultiRelationalRelation" = ' . $relation, $dq->sql());
|
||||||
|
$this->assertStringContainsString($joinAliasWithQuotes . '."MultiRelationalClass" = "DataQueryTest_ObjectHasMultiRelationalHasMany"."ClassName"', $dq->sql());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideApplyRelationMultiRelational(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'relation1' => [
|
||||||
|
'relation' => 'MultiRelational1',
|
||||||
|
],
|
||||||
|
'relation2' => [
|
||||||
|
'relation' => 'MultiRelational2',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function testApplyRelationDeepInheritance()
|
public function testApplyRelationDeepInheritance()
|
||||||
{
|
{
|
||||||
//test has_one relation
|
//test has_one relation
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\DataQueryTest;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
|
||||||
|
class ObjectHasMultiRelationalHasMany extends DataObject implements TestOnly
|
||||||
|
{
|
||||||
|
private static string $table_name = 'DataQueryTest_ObjectHasMultiRelationalHasMany';
|
||||||
|
|
||||||
|
private static array $db = [
|
||||||
|
'Name' => 'Varchar',
|
||||||
|
'SortOrder' => 'Int',
|
||||||
|
];
|
||||||
|
|
||||||
|
private static array $has_many = [
|
||||||
|
'MultiRelational1' => ObjectHasMultiRelationalHasOne::class . '.MultiRelational',
|
||||||
|
'MultiRelational2' => ObjectHasMultiRelationalHasOne::class . '.MultiRelational',
|
||||||
|
];
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\DataQueryTest;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\ORM\DataObjectSchema;
|
||||||
|
|
||||||
|
class ObjectHasMultiRelationalHasOne extends DataObject implements TestOnly
|
||||||
|
{
|
||||||
|
private static string $table_name = 'DataQueryTest_ObjectHasMultiRelationalHasOne';
|
||||||
|
|
||||||
|
private static array $db = [
|
||||||
|
'Name' => 'Varchar',
|
||||||
|
'SortOrder' => 'Int',
|
||||||
|
];
|
||||||
|
|
||||||
|
private static array $has_one = [
|
||||||
|
'MultiRelational' => [
|
||||||
|
'class' => DataObject::class,
|
||||||
|
DataObjectSchema::HAS_ONE_MULTI_RELATIONAL => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
@ -15,7 +15,10 @@ class PolymorphicHasManyListTest extends SapphireTest
|
|||||||
{
|
{
|
||||||
|
|
||||||
// Borrow the model from DataObjectTest
|
// Borrow the model from DataObjectTest
|
||||||
protected static $fixture_file = 'DataObjectTest.yml';
|
protected static $fixture_file = [
|
||||||
|
'DataObjectTest.yml',
|
||||||
|
'PolymorphicHasManyListTest.yml',
|
||||||
|
];
|
||||||
|
|
||||||
public static function getExtraDataObjects()
|
public static function getExtraDataObjects()
|
||||||
{
|
{
|
||||||
@ -32,6 +35,30 @@ class PolymorphicHasManyListTest extends SapphireTest
|
|||||||
$this->assertEquals([], $newTeam->Fans()->column('ID'));
|
$this->assertEquals([], $newTeam->Fans()->column('ID'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that multiple has_many relations can point to a single multi-relational
|
||||||
|
* has_one relation and still be separate
|
||||||
|
*/
|
||||||
|
public function testMultiRelationalRelations(): void
|
||||||
|
{
|
||||||
|
$team = $this->objFromFixture(Team::class, 'multiRelationalTeam');
|
||||||
|
$playersList1 = $team->ManyPlayers1();
|
||||||
|
$playersList2 = $team->ManyPlayers2();
|
||||||
|
|
||||||
|
// Lists are separate
|
||||||
|
$this->assertSame(
|
||||||
|
['MultiRelational Player 1', 'MultiRelational Player 2', 'MultiRelational Player 6'],
|
||||||
|
$playersList1->sort('FirstName')->column('FirstName')
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
['MultiRelational Player 3', 'MultiRelational Player 4', 'MultiRelational Player 5'],
|
||||||
|
$playersList2->sort('FirstName')->column('FirstName')
|
||||||
|
);
|
||||||
|
// The relation is saved on the has_one side of the relationship
|
||||||
|
$this->assertSame('ManyPlayers1', $playersList1->first()->MultiRelationalRelation);
|
||||||
|
$this->assertSame('ManyPlayers2', $playersList2->first()->MultiRelationalRelation);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test that DataList::relation works with PolymorphicHasManyList
|
* Test that DataList::relation works with PolymorphicHasManyList
|
||||||
*/
|
*/
|
||||||
@ -40,7 +67,7 @@ class PolymorphicHasManyListTest extends SapphireTest
|
|||||||
// Check that expected teams exist
|
// Check that expected teams exist
|
||||||
$list = Team::get();
|
$list = Team::get();
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
['Subteam 1', 'Subteam 2', 'Subteam 3', 'Team 1', 'Team 2', 'Team 3'],
|
['MultiRelational team', 'Subteam 1', 'Subteam 2', 'Subteam 3', 'Team 1', 'Team 2', 'Team 3'],
|
||||||
$list->sort('Title')->column('Title')
|
$list->sort('Title')->column('Title')
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -65,16 +92,60 @@ class PolymorphicHasManyListTest extends SapphireTest
|
|||||||
$this->assertEquals(['Bobby', 'Damian', 'Mindy', 'Mitch', 'Richard'], $fans->sort('Name')->column('Name'));
|
$this->assertEquals(['Bobby', 'Damian', 'Mindy', 'Mitch', 'Richard'], $fans->sort('Name')->column('Name'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same as testFilterRelation but for multi-relational relationships
|
||||||
|
*/
|
||||||
|
public function testFilterMultiRelationalRelation(): void
|
||||||
|
{
|
||||||
|
$list = Team::get();
|
||||||
|
|
||||||
|
$players1 = $list->relation('ManyPlayers1')->sort('FirstName')->column('FirstName');
|
||||||
|
$players2 = $list->relation('ManyPlayers2')->sort('FirstName')->column('FirstName');
|
||||||
|
// Test that each relation has the expected players
|
||||||
|
$this->assertSame(
|
||||||
|
['MultiRelational Player 1', 'MultiRelational Player 2', 'MultiRelational Player 6'],
|
||||||
|
$players1
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
['MultiRelational Player 3', 'MultiRelational Player 4', 'MultiRelational Player 5'],
|
||||||
|
$players2
|
||||||
|
);
|
||||||
|
|
||||||
|
// Modify list of fans
|
||||||
|
$team = $this->objFromFixture(DataObjectTest\Team::class, 'multiRelationalTeam');
|
||||||
|
$newPlayer1 = new DataObjectTest\Player(['FirstName' => 'New player 1']);
|
||||||
|
$team->ManyPlayers1()->add($newPlayer1);
|
||||||
|
$this->assertSame('ManyPlayers1', $newPlayer1->MultiRelationalRelation);
|
||||||
|
$this->assertSame(Team::class, $newPlayer1->MultiRelationalClass);
|
||||||
|
$this->assertSame($team->ID, $newPlayer1->MultiRelationalID);
|
||||||
|
$newPlayer2 = new DataObjectTest\Player(['FirstName' => 'New player 2']);
|
||||||
|
$team->ManyPlayers2()->add($newPlayer2);
|
||||||
|
$this->assertSame('ManyPlayers2', $newPlayer2->MultiRelationalRelation);
|
||||||
|
$this->assertSame(Team::class, $newPlayer2->MultiRelationalClass);
|
||||||
|
$this->assertSame($team->ID, $newPlayer2->MultiRelationalID);
|
||||||
|
|
||||||
|
// and retest
|
||||||
|
$players1 = $list->relation('ManyPlayers1')->sort('FirstName')->column('FirstName');
|
||||||
|
$players2 = $list->relation('ManyPlayers2')->sort('FirstName')->column('FirstName');
|
||||||
|
$this->assertSame(
|
||||||
|
['MultiRelational Player 1', 'MultiRelational Player 2', 'MultiRelational Player 6', 'New player 1'],
|
||||||
|
$players1
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
['MultiRelational Player 3', 'MultiRelational Player 4', 'MultiRelational Player 5', 'New player 2'],
|
||||||
|
$players2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test that related objects can be removed from a relation
|
* Test that related objects can be removed from a relation
|
||||||
*/
|
*/
|
||||||
public function testRemoveRelation()
|
public function testRemoveRelation()
|
||||||
{
|
{
|
||||||
|
|
||||||
// Check that expected teams exist
|
// Check that expected teams exist
|
||||||
$list = Team::get();
|
$list = Team::get();
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
['Subteam 1', 'Subteam 2', 'Subteam 3', 'Team 1', 'Team 2', 'Team 3'],
|
['MultiRelational team', 'Subteam 1', 'Subteam 2', 'Subteam 3', 'Team 1', 'Team 2', 'Team 3'],
|
||||||
$list->sort('Title')->column('Title')
|
$list->sort('Title')->column('Title')
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -109,10 +180,57 @@ class PolymorphicHasManyListTest extends SapphireTest
|
|||||||
$this->assertEmpty($subteam1fan->FavouriteClass);
|
$this->assertEmpty($subteam1fan->FavouriteClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same as testRemoveRelation but for multi-relational relationships
|
||||||
|
*/
|
||||||
|
public function testRemoveMultiRelationalRelation(): void
|
||||||
|
{
|
||||||
|
$team = $this->objFromFixture(DataObjectTest\Team::class, 'multiRelationalTeam');
|
||||||
|
$originalPlayers1 = $team->ManyPlayers1()->sort('FirstName')->column('FirstName');
|
||||||
|
$originalPlayers2 = $team->ManyPlayers2()->sort('FirstName')->column('FirstName');
|
||||||
|
|
||||||
|
// Test that each relation has the expected players as a baseline
|
||||||
|
$this->assertSame(
|
||||||
|
['MultiRelational Player 1', 'MultiRelational Player 2', 'MultiRelational Player 6'],
|
||||||
|
$originalPlayers1
|
||||||
|
);
|
||||||
|
$this->assertSame(
|
||||||
|
['MultiRelational Player 3', 'MultiRelational Player 4', 'MultiRelational Player 5'],
|
||||||
|
$originalPlayers2
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test that you can't remove items from relations they're not in
|
||||||
|
$playerFromGroup1 = $this->objFromFixture(DataObjectTest\Player::class, 'multiRelationalPlayer2');
|
||||||
|
$team->ManyPlayers2()->remove($playerFromGroup1);
|
||||||
|
$this->assertSame($originalPlayers1, $team->ManyPlayers1()->sort('FirstName')->column('FirstName'));
|
||||||
|
$this->assertSame($originalPlayers2, $team->ManyPlayers2()->sort('FirstName')->column('FirstName'));
|
||||||
|
$this->assertSame('ManyPlayers1', $playerFromGroup1->MultiRelationalRelation);
|
||||||
|
$this->assertSame(Team::class, $playerFromGroup1->MultiRelationalClass);
|
||||||
|
$this->assertSame($team->ID, $playerFromGroup1->MultiRelationalID);
|
||||||
|
|
||||||
|
// Test that you *can* remove items from relations they *are* in
|
||||||
|
$team->ManyPlayers1()->remove($playerFromGroup1);
|
||||||
|
$this->assertSame(
|
||||||
|
['MultiRelational Player 1', 'MultiRelational Player 6'],
|
||||||
|
$team->ManyPlayers1()->sort('FirstName')->column('FirstName')
|
||||||
|
);
|
||||||
|
$this->assertSame($originalPlayers2, $team->ManyPlayers2()->sort('FirstName')->column('FirstName'));
|
||||||
|
$this->assertEmpty($playerFromGroup1->MultiRelationalRelation);
|
||||||
|
$this->assertEmpty($playerFromGroup1->MultiRelationalClass);
|
||||||
|
$this->assertEmpty($playerFromGroup1->MultiRelationalID);
|
||||||
|
}
|
||||||
|
|
||||||
public function testGetForeignClassKey(): void
|
public function testGetForeignClassKey(): void
|
||||||
{
|
{
|
||||||
$team = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
|
$team = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
|
||||||
$list = $team->Fans();
|
$list = $team->Fans();
|
||||||
$this->assertSame('FavouriteClass', $list->getForeignClassKey());
|
$this->assertSame('FavouriteClass', $list->getForeignClassKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getGetForeignRelationKey(): void
|
||||||
|
{
|
||||||
|
$team = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
|
||||||
|
$list = $team->Fans();
|
||||||
|
$this->assertSame('FavouriteRelation', $list->getForeignRelationKey());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
25
tests/php/ORM/PolymorphicHasManyListTest.yml
Normal file
25
tests/php/ORM/PolymorphicHasManyListTest.yml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
SilverStripe\ORM\Tests\DataObjectTest\Player:
|
||||||
|
multiRelationalPlayer1:
|
||||||
|
FirstName: MultiRelational Player 1
|
||||||
|
multiRelationalPlayer2:
|
||||||
|
FirstName: MultiRelational Player 2
|
||||||
|
multiRelationalPlayer3:
|
||||||
|
FirstName: MultiRelational Player 3
|
||||||
|
multiRelationalPlayer4:
|
||||||
|
FirstName: MultiRelational Player 4
|
||||||
|
multiRelationalPlayer5:
|
||||||
|
FirstName: MultiRelational Player 5
|
||||||
|
multiRelationalPlayer6:
|
||||||
|
FirstName: MultiRelational Player 6
|
||||||
|
|
||||||
|
SilverStripe\ORM\Tests\DataObjectTest\Team:
|
||||||
|
multiRelationalTeam:
|
||||||
|
Title: MultiRelational team
|
||||||
|
ManyPlayers1:
|
||||||
|
- =>SilverStripe\ORM\Tests\DataObjectTest\Player.multiRelationalPlayer1
|
||||||
|
- =>SilverStripe\ORM\Tests\DataObjectTest\Player.multiRelationalPlayer2
|
||||||
|
- =>SilverStripe\ORM\Tests\DataObjectTest\Player.multiRelationalPlayer6
|
||||||
|
ManyPlayers2:
|
||||||
|
- =>SilverStripe\ORM\Tests\DataObjectTest\Player.multiRelationalPlayer3
|
||||||
|
- =>SilverStripe\ORM\Tests\DataObjectTest\Player.multiRelationalPlayer4
|
||||||
|
- =>SilverStripe\ORM\Tests\DataObjectTest\Player.multiRelationalPlayer5
|
Loading…
Reference in New Issue
Block a user