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
|
||||
PolymorphicForeignKey:
|
||||
class: SilverStripe\ORM\FieldType\DBPolymorphicForeignKey
|
||||
PolymorphicRelationAwareForeignKey:
|
||||
class: SilverStripe\ORM\FieldType\DBPolymorphicRelationAwareForeignKey
|
||||
PrimaryKey:
|
||||
class: SilverStripe\ORM\FieldType\DBPrimaryKey
|
||||
Text:
|
||||
|
@ -2,12 +2,14 @@
|
||||
|
||||
namespace SilverStripe\Dev\Validation;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use ReflectionException;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use SilverStripe\Core\Config\Configurable;
|
||||
use SilverStripe\Core\Injector\Injectable;
|
||||
use SilverStripe\Core\Resettable;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataObjectSchema;
|
||||
use SilverStripe\ORM\DB;
|
||||
|
||||
/**
|
||||
@ -291,6 +293,25 @@ class RelationValidationService implements Resettable
|
||||
$relations = (array) $singleton->config()->uninherited('has_one');
|
||||
|
||||
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)) {
|
||||
continue;
|
||||
}
|
||||
@ -305,6 +326,11 @@ class RelationValidationService implements Resettable
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip checking for back relations when has_one is polymorphic
|
||||
if ($relationData === DataObject::class) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_subclass_of($relationData, DataObject::class)) {
|
||||
$this->logError(
|
||||
$class,
|
||||
@ -616,7 +642,8 @@ class RelationValidationService implements Resettable
|
||||
return null;
|
||||
}
|
||||
|
||||
return $throughRelations[$to];
|
||||
$spec = $throughRelations[$to];
|
||||
return is_array($spec) ? $spec['class'] ?? null : $spec;
|
||||
}
|
||||
|
||||
return $relationData;
|
||||
|
@ -204,6 +204,14 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler
|
||||
$classKey = $list->getForeignClassKey();
|
||||
$class = $list->getForeignClass();
|
||||
$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\Resettable;
|
||||
use SilverStripe\Dev\Debug;
|
||||
use SilverStripe\Dev\Deprecation;
|
||||
use SilverStripe\Forms\FieldList;
|
||||
use SilverStripe\Forms\FormField;
|
||||
use SilverStripe\Forms\FormScaffolder;
|
||||
@ -1972,12 +1973,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
|
||||
// Determine type and nature of foreign relation
|
||||
$joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
|
||||
/** @var HasManyList $result */
|
||||
if ($polymorphic) {
|
||||
$result = PolymorphicHasManyList::create($componentClass, $joinField, static::class);
|
||||
$details = $schema->getHasManyComponentDetails(static::class, $componentName);
|
||||
if ($details['polymorphic']) {
|
||||
$result = PolymorphicHasManyList::create($componentClass, $details['joinField'], static::class);
|
||||
if ($details['needsRelation']) {
|
||||
Deprecation::withNoReplacement(fn () => $result->setForeignRelation($componentName));
|
||||
}
|
||||
} else {
|
||||
$result = HasManyList::create($componentClass, $joinField);
|
||||
$result = HasManyList::create($componentClass, $details['joinField']);
|
||||
}
|
||||
|
||||
return $result
|
||||
@ -1993,16 +1996,21 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*/
|
||||
public function getRelationClass($relationName)
|
||||
{
|
||||
// Parse many_many
|
||||
$manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
|
||||
// Parse many_many, which can have an array instead of a class name
|
||||
$manyManyComponent = static::getSchema()->manyManyComponent(static::class, $relationName);
|
||||
if ($manyManyComponent) {
|
||||
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();
|
||||
$candidates = array_merge(
|
||||
($relations = $config->get('has_one')) ? $relations : [],
|
||||
($relations = $config->get('has_many')) ? $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
|
||||
* their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
|
||||
* Return the class of a all has_one relations.
|
||||
*
|
||||
* @return string|array The class of the one-to-one component, or an array of all one-to-one components and
|
||||
* their classes.
|
||||
* @return array An array of all has_one components and their classes.
|
||||
*/
|
||||
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 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
|
||||
* databases that do not support some characters.
|
||||
@ -501,7 +506,20 @@ class DataObjectSchema
|
||||
|
||||
// Add in all has_ones
|
||||
$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) {
|
||||
$compositeFields[$fieldName] = 'PolymorphicForeignKey';
|
||||
} else {
|
||||
@ -902,12 +920,34 @@ class DataObjectSchema
|
||||
return null;
|
||||
}
|
||||
|
||||
$spec = $hasOnes[$component];
|
||||
|
||||
// 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');
|
||||
|
||||
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.
|
||||
*
|
||||
@ -1047,6 +1087,20 @@ class DataObjectSchema
|
||||
*/
|
||||
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
|
||||
if ($type === 'has_many') {
|
||||
$remoteClass = $this->hasManyComponent($class, $component, false);
|
||||
@ -1071,6 +1125,11 @@ class DataObjectSchema
|
||||
|
||||
// Reference remote has_one to check against
|
||||
$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
|
||||
// with the same type as the current class
|
||||
@ -1104,14 +1163,23 @@ class DataObjectSchema
|
||||
on class {$class}");
|
||||
}
|
||||
|
||||
// Inspect resulting found relation
|
||||
if ($remoteRelations[$remoteField] === DataObject::class) {
|
||||
$polymorphic = true;
|
||||
return $remoteField; // Composite polymorphic field does not include 'ID' suffix
|
||||
} else {
|
||||
$polymorphic = false;
|
||||
return $remoteField . 'ID';
|
||||
$polymorphic = $this->hasOneComponent($remoteClass, $remoteField) === DataObject::class;
|
||||
$remoteClassField = $polymorphic ? $remoteField . 'Class' : null;
|
||||
$needsRelation = $type === 'has_many' && $polymorphic && $this->hasOneComponentHandlesMultipleRelations($remoteClass, $remoteField);
|
||||
$remoteRelationField = $needsRelation ? $remoteField . 'Relation' : null;
|
||||
|
||||
// This must be after the above assignments, as they rely on the original value.
|
||||
if (!$polymorphic) {
|
||||
$remoteField .= 'ID';
|
||||
}
|
||||
|
||||
return [
|
||||
'joinField' => $remoteField,
|
||||
'relationField' => $remoteRelationField,
|
||||
'classField' => $remoteClassField,
|
||||
'polymorphic' => $polymorphic,
|
||||
'needsRelation' => $needsRelation,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1204,6 +1272,24 @@ class DataObjectSchema
|
||||
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
|
||||
*
|
||||
|
@ -1025,8 +1025,6 @@ class DataQuery
|
||||
* Join the given has_many relation to this query.
|
||||
* 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 $localField Name of the has_many relationship to join
|
||||
* @param string $foreignClass Class to join
|
||||
@ -1065,6 +1063,15 @@ class DataQuery
|
||||
$localClassColumn = $schema->sqlColumnForField($localClass, 'ClassName', $localPrefix);
|
||||
$joinExpression =
|
||||
"{$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 {
|
||||
$foreignKeyIDColumn = $schema->sqlColumnForField($foreignClass, $foreignKey, $foreignPrefix);
|
||||
$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\Convert;
|
||||
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
|
||||
{
|
||||
@ -20,7 +22,13 @@ class PolymorphicHasManyList extends HasManyList
|
||||
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
|
||||
*/
|
||||
@ -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
|
||||
{
|
||||
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.
|
||||
*
|
||||
* @param string $dataClass The class of the DataObjects that this will list.
|
||||
* @param string $foreignField The name of the composite foreign relation field. Used
|
||||
* to generate the ID and Class foreign keys.
|
||||
* @param string $foreignField The name of the composite foreign (has_one) relation field. Used
|
||||
* to generate the ID, Class, and Relation foreign keys.
|
||||
* @param string $foreignClass Name of the class filter this relation is filtered against
|
||||
*/
|
||||
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
|
||||
parent::__construct($dataClass, "{$foreignField}ID");
|
||||
$this->classForeignKey = "{$foreignField}Class";
|
||||
$this->relationForeignKey = "{$foreignField}Relation";
|
||||
|
||||
// Ensure underlying DataQuery globally references the class filter
|
||||
$this->dataQuery->setQueryParam('Foreign.Class', $foreignClass);
|
||||
@ -98,11 +139,19 @@ class PolymorphicHasManyList extends HasManyList
|
||||
return;
|
||||
}
|
||||
|
||||
// set the {$relationName}Class field value
|
||||
$foreignKey = $this->foreignKey;
|
||||
$classForeignKey = $this->classForeignKey;
|
||||
$item->$foreignKey = $foreignID;
|
||||
$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();
|
||||
}
|
||||
|
||||
@ -129,6 +178,13 @@ class PolymorphicHasManyList extends HasManyList
|
||||
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
|
||||
$foreignID = $this->getForeignID();
|
||||
$foreignKey = $this->foreignKey;
|
||||
@ -137,9 +193,20 @@ class PolymorphicHasManyList extends HasManyList
|
||||
|| $foreignID == $item->$foreignKey
|
||||
|| (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->$classForeignKey = null;
|
||||
$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 = [
|
||||
'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\Dev\SapphireTest;
|
||||
use SilverStripe\Dev\Validation\RelationValidationService;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataObjectSchema;
|
||||
|
||||
class RelationValidationTest extends SapphireTest
|
||||
{
|
||||
@ -107,6 +109,14 @@ class RelationValidationTest extends SapphireTest
|
||||
'SilverStripe\Dev\Tests\Validation\Member / Hat : Back relation is ambiguous',
|
||||
],
|
||||
],
|
||||
'polymorphic has one' => [
|
||||
Team::class,
|
||||
'has_one',
|
||||
[
|
||||
'SingleMember' => DataObject::class,
|
||||
],
|
||||
[],
|
||||
],
|
||||
'invalid has one' => [
|
||||
Member::class,
|
||||
'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).'
|
||||
],
|
||||
],
|
||||
'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' => [
|
||||
Team::class,
|
||||
'has_many',
|
||||
|
@ -4,6 +4,7 @@ namespace SilverStripe\Dev\Tests\Validation;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataObjectSchema;
|
||||
use SilverStripe\ORM\HasManyList;
|
||||
use SilverStripe\ORM\ManyManyList;
|
||||
use SilverStripe\ORM\ManyManyThroughList;
|
||||
@ -31,6 +32,13 @@ class Team extends DataObject implements TestOnly
|
||||
'Title' => 'Varchar(255)',
|
||||
];
|
||||
|
||||
private static array $has_one = [
|
||||
'SingleMember' => [
|
||||
'class' => DataObject::class,
|
||||
DataObjectSchema::HAS_ONE_MULTI_RELATIONAL => true,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
|
@ -13,6 +13,7 @@ use SilverStripe\Forms\HiddenField;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\Category;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\CategoryController;
|
||||
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\Person;
|
||||
use SilverStripe\Forms\Tests\GridField\GridFieldDetailFormTest\PolymorphicPeopleGroup;
|
||||
@ -26,6 +27,7 @@ class GridFieldDetailFormTest extends FunctionalTest
|
||||
Person::class,
|
||||
PeopleGroup::class,
|
||||
PolymorphicPeopleGroup::class,
|
||||
MultiRelationalPeopleGroup::class,
|
||||
Category::class,
|
||||
];
|
||||
|
||||
@ -143,6 +145,31 @@ class GridFieldDetailFormTest extends FunctionalTest
|
||||
$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()
|
||||
{
|
||||
$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\RequiredFields;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataObjectSchema;
|
||||
|
||||
class Person extends DataObject implements TestOnly
|
||||
{
|
||||
@ -20,6 +21,10 @@ class Person extends DataObject implements TestOnly
|
||||
private static $has_one = [
|
||||
'Group' => PeopleGroup::class,
|
||||
'PolymorphicGroup' => DataObject::class,
|
||||
'MultiRelationalGroup' => [
|
||||
'class' => DataObject::class,
|
||||
DataObjectSchema::HAS_ONE_MULTI_RELATIONAL => true,
|
||||
],
|
||||
];
|
||||
|
||||
private static $many_many = [
|
||||
|
@ -16,7 +16,7 @@ class PolymorphicPeopleGroup extends DataObject implements TestOnly
|
||||
];
|
||||
|
||||
private static $has_many = [
|
||||
'People' => Person::class
|
||||
'People' => Person::class . '.PolymorphicGroup'
|
||||
];
|
||||
|
||||
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();
|
||||
$this->assertEquals(
|
||||
[
|
||||
'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
|
||||
)
|
||||
);
|
||||
$this->assertEquals($expected, $schema->fieldSpecs(...$args));
|
||||
}
|
||||
|
||||
// Use all options at once
|
||||
$this->assertEquals(
|
||||
[
|
||||
'ID' => DataObjectSchemaTest\HasFields::class . '.PrimaryKey',
|
||||
'Description' => DataObjectSchemaTest\HasFields::class . '.Varchar',
|
||||
'MoneyFieldCurrency' => DataObjectSchemaTest\HasFields::class . '.Varchar(3)',
|
||||
'MoneyFieldAmount' => DataObjectSchemaTest\HasFields::class . '.Decimal(19,4)',
|
||||
public function provideFieldSpec(): array
|
||||
{
|
||||
return [
|
||||
'just pass a class' => [
|
||||
'args' => [HasFields::class],
|
||||
'expected' => [
|
||||
'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,
|
||||
DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED
|
||||
)
|
||||
);
|
||||
'prefix with classname' => [
|
||||
'args' => [HasFields::class, DataObjectSchema::INCLUDE_CLASS],
|
||||
'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()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataObjectSchema;
|
||||
|
||||
class WithRelation extends NoFields
|
||||
{
|
||||
private static $table_name = 'DataObjectSchemaTest_WithRelation';
|
||||
|
||||
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;
|
||||
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\ORM\DataObject;
|
||||
use SilverStripe\ORM\DataObjectSchema;
|
||||
use SilverStripe\ORM\Tests\DataObjectTest;
|
||||
use SilverStripe\Security\Member;
|
||||
|
||||
@ -17,6 +19,10 @@ class Player extends Member implements TestOnly
|
||||
|
||||
private static $has_one = [
|
||||
'FavouriteTeam' => DataObjectTest\Team::class,
|
||||
'MultiRelational' => [
|
||||
'class' => DataObject::class,
|
||||
DataObjectSchema::HAS_ONE_MULTI_RELATIONAL => true,
|
||||
],
|
||||
];
|
||||
|
||||
private static $belongs_many_many = [
|
||||
|
@ -44,7 +44,10 @@ class Team extends DataObject implements TestOnly
|
||||
'SubTeams' => SubTeam::class,
|
||||
'Comments' => TeamComment::class,
|
||||
'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 = [
|
||||
|
@ -27,6 +27,8 @@ class DataQueryTest extends SapphireTest
|
||||
DataQueryTest\ObjectG::class,
|
||||
DataQueryTest\ObjectH::class,
|
||||
DataQueryTest\ObjectI::class,
|
||||
DataQueryTest\ObjectHasMultiRelationalHasOne::class,
|
||||
DataQueryTest\ObjectHasMultiRelationalHasMany::class,
|
||||
SQLSelectTest\CteRecursiveObject::class,
|
||||
SQLSelectTest\TestObject::class,
|
||||
SQLSelectTest\TestBase::class,
|
||||
@ -99,6 +101,33 @@ class DataQueryTest extends SapphireTest
|
||||
$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()
|
||||
{
|
||||
//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
|
||||
protected static $fixture_file = 'DataObjectTest.yml';
|
||||
protected static $fixture_file = [
|
||||
'DataObjectTest.yml',
|
||||
'PolymorphicHasManyListTest.yml',
|
||||
];
|
||||
|
||||
public static function getExtraDataObjects()
|
||||
{
|
||||
@ -32,6 +35,30 @@ class PolymorphicHasManyListTest extends SapphireTest
|
||||
$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
|
||||
*/
|
||||
@ -40,7 +67,7 @@ class PolymorphicHasManyListTest extends SapphireTest
|
||||
// Check that expected teams exist
|
||||
$list = Team::get();
|
||||
$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')
|
||||
);
|
||||
|
||||
@ -65,16 +92,60 @@ class PolymorphicHasManyListTest extends SapphireTest
|
||||
$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
|
||||
*/
|
||||
public function testRemoveRelation()
|
||||
{
|
||||
|
||||
// Check that expected teams exist
|
||||
$list = Team::get();
|
||||
$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')
|
||||
);
|
||||
|
||||
@ -109,10 +180,57 @@ class PolymorphicHasManyListTest extends SapphireTest
|
||||
$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
|
||||
{
|
||||
$team = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
|
||||
$list = $team->Fans();
|
||||
$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