-
Notifications
You must be signed in to change notification settings - Fork 125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NEW Add support for ManyManyThrough relations #260
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
|
||
namespace Symbiote\GridFieldExtensions; | ||
|
||
use Exception; | ||
use SilverStripe\Control\Controller; | ||
use SilverStripe\Control\RequestHandler; | ||
use SilverStripe\Core\ClassInfo; | ||
|
@@ -17,9 +18,11 @@ | |
use SilverStripe\ORM\DataList; | ||
use SilverStripe\ORM\DataObject; | ||
use SilverStripe\ORM\DataObjectInterface; | ||
use SilverStripe\ORM\DataObjectSchema; | ||
use SilverStripe\ORM\DB; | ||
use SilverStripe\ORM\ManyManyList; | ||
use SilverStripe\ORM\Map; | ||
use SilverStripe\ORM\ManyManyThroughList; | ||
use SilverStripe\ORM\ManyManyThroughQueryManipulator; | ||
use SilverStripe\ORM\SS_List; | ||
use SilverStripe\ORM\FieldType\DBDatetime; | ||
use SilverStripe\Versioned\Versioned; | ||
|
@@ -141,6 +144,16 @@ public function getExtraSortFields() | |
return $this->extraSortFields; | ||
} | ||
|
||
/** | ||
* Checks to see if the relationship list is for a type of many_many | ||
* | ||
* @param SS_List $list | ||
*/ | ||
protected function isManyMany(SS_List $list) | ||
{ | ||
return $list instanceof ManyManyList || $list instanceof ManyManyThroughList; | ||
} | ||
|
||
/** | ||
* Sets extra sort fields to apply before the sort field. | ||
* | ||
|
@@ -183,12 +196,19 @@ public function validateSortField(SS_List $list) | |
{ | ||
$field = $this->getSortField(); | ||
|
||
// Check extra fields on many many relation types | ||
if ($list instanceof ManyManyList) { | ||
$extra = $list->getExtraFields(); | ||
|
||
if ($extra && array_key_exists($field, $extra)) { | ||
return; | ||
} | ||
} elseif ($list instanceof ManyManyThroughList) { | ||
$manipulator = $this->getManyManyInspector($list); | ||
$fieldTable = DataObject::getSchema()->tableForField($manipulator->getJoinClass(), $field); | ||
if ($fieldTable) { | ||
return; | ||
} | ||
} | ||
|
||
$classes = ClassInfo::dataClassesFor($list->dataClass()); | ||
|
@@ -199,7 +219,7 @@ public function validateSortField(SS_List $list) | |
} | ||
} | ||
|
||
throw new \Exception("Couldn't find the sort field '" . $field . "'"); | ||
throw new Exception("Couldn't find the sort field '" . $field . "'"); | ||
} | ||
|
||
/** | ||
|
@@ -217,14 +237,16 @@ public function getSortTable(SS_List $list) | |
if ($extra && array_key_exists($field, $extra)) { | ||
return $table; | ||
} | ||
} elseif ($list instanceof ManyManyThroughList) { | ||
return $this->getManyManyInspector($list)->getJoinAlias(); | ||
} | ||
$classes = ClassInfo::dataClassesFor($list->dataClass()); | ||
foreach ($classes as $class) { | ||
if (singleton($class)->hasDataBaseField($field)) { | ||
return DataObject::getSchema()->tableName($class); | ||
} | ||
} | ||
throw new \Exception("Couldn't find the sort field '$field'"); | ||
throw new Exception("Couldn't find the sort field '$field'"); | ||
} | ||
|
||
public function getURLHandlers($grid) | ||
|
@@ -340,9 +362,10 @@ public function handleReorder($grid, $request) | |
} | ||
$list = $grid->getList(); | ||
$modelClass = $grid->getModelClass(); | ||
if ($list instanceof ManyManyList && !singleton($modelClass)->canView()) { | ||
$isManyMany = $this->isManyMany($list); | ||
if ($isManyMany && !singleton($modelClass)->canView()) { | ||
$this->httpError(403); | ||
} elseif (!($list instanceof ManyManyList) && !singleton($modelClass)->canEdit()) { | ||
} elseif (!$isManyMany && !singleton($modelClass)->canEdit()) { | ||
$this->httpError(403); | ||
} | ||
|
||
|
@@ -364,10 +387,10 @@ public function handleReorder($grid, $request) | |
} | ||
|
||
/** | ||
* Get mapping of sort value to ID from posted data | ||
* Get mapping of sort value to item ID from posted data (gridfield list state), ordered by sort value. | ||
* | ||
* @param array $data Raw posted data | ||
* @return array | ||
* @return array [sortIndex => recordID] | ||
*/ | ||
protected function getSortedIDs($data) | ||
{ | ||
|
@@ -473,7 +496,7 @@ protected function executeReorder(GridField $grid, $sortedIDs) | |
if (!is_array($sortedIDs)) { | ||
return false; | ||
} | ||
$field = $this->getSortField(); | ||
$sortField = $this->getSortField(); | ||
|
||
$sortterm = ''; | ||
if ($this->extraSortFields) { | ||
|
@@ -486,7 +509,7 @@ protected function executeReorder(GridField $grid, $sortedIDs) | |
} | ||
} | ||
$list = $grid->getList(); | ||
$sortterm .= '"'.$this->getSortTable($list).'"."'.$field.'"'; | ||
$sortterm .= '"'.$this->getSortTable($list).'"."'.$sortField.'"'; | ||
$items = $list->filter('ID', $sortedIDs)->sort($sortterm); | ||
|
||
// Ensure that each provided ID corresponded to an actual object. | ||
|
@@ -507,47 +530,73 @@ protected function executeReorder(GridField $grid, $sortedIDs) | |
if (isset($record->_SortColumn0)) { | ||
$current[$record->ID] = $record->_SortColumn0; | ||
} else { | ||
$current[$record->ID] = $record->$field; | ||
$current[$record->ID] = $record->$sortField; | ||
} | ||
} | ||
} elseif ($items instanceof ManyManyThroughList) { | ||
$manipulator = $this->getManyManyInspector($list); | ||
$joinClass = $manipulator->getJoinClass(); | ||
$fromRelationName = $manipulator->getForeignKey(); | ||
$toRelationName = $manipulator->getLocalKey(); | ||
$sortlist = DataList::create($joinClass)->filter([ | ||
$toRelationName => $items->column('ID'), | ||
$fromRelationName => $items->first()->getJoin()->$fromRelationName, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Safe due to earlier checks (short circuit |
||
]); | ||
$current = $sortlist->map($toRelationName, $sortField)->toArray(); | ||
} else { | ||
$current = $items->map('ID', $field)->toArray(); | ||
$current = $items->map('ID', $sortField)->toArray(); | ||
} | ||
|
||
// Perform the actual re-ordering. | ||
$this->reorderItems($list, $current, $sortedIDs); | ||
return true; | ||
} | ||
|
||
/** | ||
* @param SS_List $list | ||
* @param array $values **UNUSED** [listItemID => currentSortValue]; | ||
* @param array $sortedIDs [newSortValue => listItemID] | ||
*/ | ||
protected function reorderItems($list, array $values, array $sortedIDs) | ||
{ | ||
// setup | ||
$sortField = $this->getSortField(); | ||
/** @var SS_List $map */ | ||
$map = $list->map('ID', $sortField); | ||
//fix for versions of SS that return inconsistent types for `map` function | ||
if ($map instanceof Map) { | ||
$map = $map->toArray(); | ||
} | ||
$class = $list->dataClass(); | ||
// The problem is that $sortedIDs is a list of the _related_ item IDs, which causes trouble | ||
// with ManyManyThrough, where we need the ID of the _join_ item in order to set the value. | ||
$itemToSortReference = ($list instanceof ManyManyThroughList) ? 'getJoin' : 'Me'; | ||
$currentSortList = $list->map('ID', $itemToSortReference)->toArray(); | ||
|
||
// If not a ManyManyList and using versioning, detect it. | ||
// sanity check. | ||
$this->validateSortField($list); | ||
$isVersioned = false; | ||
$class = $list->dataClass(); | ||
|
||
$isVersioned = false; | ||
// check if sort column is present on the model provided by dataClass() and if it's versioned | ||
// cases: | ||
// Model has sort column and is versioned - handle as versioned | ||
// Model has sort column and is NOT versioned - handle as NOT versioned | ||
// Model doesn't have sort column because sort column is on ManyManyList - handle as NOT versioned | ||
|
||
// try to match table name, note that we have to cover the case where the table which has the sort column | ||
// belongs to ancestor of the object which is populating the list | ||
$classes = ClassInfo::ancestry($class, true); | ||
foreach ($classes as $currentClass) { | ||
if (DataObject::getSchema()->tableName($currentClass) == $this->getSortTable($list)) { | ||
$isVersioned = $class::has_extension(Versioned::class); | ||
break; | ||
// Model doesn't have sort column because sort column is on ManyManyThroughList... | ||
// - Related item is not versioned: | ||
// - Through object is versioned: THROW an error. | ||
// - Through object is NOT versioned: handle as NOT versioned | ||
// - Related item is versioned... | ||
// - Through object is versioned: handle as versioned | ||
// - Through object is NOT versioned: THROW an error. | ||
$isManyMany = $this->isManyMany($list); | ||
if ($isManyMany && $list instanceof ManyManyThroughList) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
$listClassVersioned = $class::create()->hasExtension(Versioned::class); | ||
// We'll be updating the join class, not the relation class. | ||
$class = $this->getManyManyInspector($list)->getJoinClass(); | ||
$isVersioned = $class::create()->hasExtension(Versioned::class); | ||
|
||
if ($listClassVersioned xor $isVersioned) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this implying that the base class and a MMT that is owns can't both be versioned? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, the opposite. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "if the list class is versioned or (never and) the data class versioned" I'm confused There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If one is versioned and the other is not, throw an error. |
||
throw new Exception( | ||
'ManyManyThrough cannot mismatch Versioning between join class and related class' | ||
); | ||
} | ||
} elseif (!$isManyMany) { | ||
$isVersioned = $class::create()->hasExtension(Versioned::class); | ||
} | ||
|
||
// Loop through each item, and update the sort values which do not | ||
|
@@ -556,43 +605,45 @@ protected function reorderItems($list, array $values, array $sortedIDs) | |
$sortTable = $this->getSortTable($list); | ||
$now = DBDatetime::now()->Rfc2822(); | ||
$additionalSQL = ''; | ||
$baseTable = DataObject::getSchema()->baseDataTable($list->dataClass()); | ||
$baseTable = DataObject::getSchema()->baseDataTable($class); | ||
|
||
$isBaseTable = ($baseTable == $sortTable); | ||
if (!$list instanceof ManyManyList && $isBaseTable) { | ||
$additionalSQL = ", \"LastEdited\" = '$now'"; | ||
} | ||
|
||
foreach ($sortedIDs as $sortValue => $id) { | ||
if ($map[$id] != $sortValue) { | ||
foreach ($sortedIDs as $newSortValue => $targetRecordID) { | ||
if ($currentSortList[$targetRecordID]->$sortField != $newSortValue) { | ||
DB::query(sprintf( | ||
'UPDATE "%s" SET "%s" = %d%s WHERE %s', | ||
$sortTable, | ||
$sortField, | ||
$sortValue, | ||
$newSortValue, | ||
$additionalSQL, | ||
$this->getSortTableClauseForIds($list, $id) | ||
$this->getSortTableClauseForIds($list, $targetRecordID) | ||
)); | ||
|
||
if (!$isBaseTable && !$list instanceof ManyManyList) { | ||
DB::query(sprintf( | ||
'UPDATE "%s" SET "LastEdited" = \'%s\' WHERE %s', | ||
$baseTable, | ||
$now, | ||
$this->getSortTableClauseForIds($list, $id) | ||
$this->getSortTableClauseForIds($list, $targetRecordID) | ||
)); | ||
} | ||
} | ||
} | ||
} else { | ||
// For versioned objects, modify them with the ORM so that the | ||
// *_versions table is updated. This ensures re-ordering works | ||
// *_Versions table is updated. This ensures re-ordering works | ||
// similar to the SiteTree where you change the position, and then | ||
// you go into the record and publish it. | ||
foreach ($sortedIDs as $sortValue => $id) { | ||
if ($map[$id] != $sortValue) { | ||
$record = $class::get()->byID($id); | ||
$record->$sortField = $sortValue; | ||
foreach ($sortedIDs as $newSortValue => $targetRecordID) { | ||
// either the list data class (has_many, (belongs_)many_many) | ||
// or the intermediary join class (many_many through) | ||
$record = $currentSortList[$targetRecordID]; | ||
if ($record->$sortField != $newSortValue) { | ||
$record->$sortField = $newSortValue; | ||
$record->write(); | ||
} | ||
} | ||
|
@@ -629,7 +680,7 @@ protected function populateSortValues(DataList $list) | |
$this->getSortTableClauseForIds($list, $id) | ||
)); | ||
|
||
if (!$isBaseTable && !$list instanceof ManyManyList) { | ||
if (!$isBaseTable && !$this->isManyMany($list)) { | ||
DB::query(sprintf( | ||
'UPDATE "%s" SET "LastEdited" = \'%s\' WHERE %s', | ||
$baseTable, | ||
|
@@ -640,6 +691,18 @@ protected function populateSortValues(DataList $list) | |
} | ||
} | ||
|
||
/** | ||
* Forms a WHERE clause for the table the sort column is defined on. | ||
* e.g. ID = 5 | ||
* e.g. ID IN(5, 8, 10) | ||
* e.g. SortOrder = 5 AND RelatedThing.ID = 3 | ||
* e.g. SortOrder IN(5, 8, 10) AND RelatedThing.ID = 3 | ||
* | ||
* @param DataList $list | ||
* @param int|string|array $ids a single number, or array of numbers | ||
* | ||
* @return string | ||
*/ | ||
protected function getSortTableClauseForIds(DataList $list, $ids) | ||
{ | ||
if (is_array($ids)) { | ||
|
@@ -648,10 +711,13 @@ protected function getSortTableClauseForIds(DataList $list, $ids) | |
$value = '= ' . (int) $ids; | ||
} | ||
|
||
if ($list instanceof ManyManyList) { | ||
$extra = $list->getExtraFields(); | ||
$key = $list->getLocalKey(); | ||
$foreignKey = $list->getForeignKey(); | ||
if ($this->isManyMany($list)) { | ||
$intropector = $this->getManyManyInspector($list); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lol 😅 |
||
$extra = $list instanceof ManyManyList ? | ||
$intropector->getExtraFields() : | ||
DataObjectSchema::create()->fieldSpecs($intropector->getJoinClass(), DataObjectSchema::DB_ONLY); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Multi line ternary isn't the best for readability, perhaps you could make this an if statement? Failing that, putting the |
||
$key = $intropector->getLocalKey(); | ||
$foreignKey = $intropector->getForeignKey(); | ||
$foreignID = (int) $list->getForeignID(); | ||
|
||
if ($extra && array_key_exists($this->getSortField(), $extra)) { | ||
|
@@ -667,4 +733,27 @@ protected function getSortTableClauseForIds(DataList $list, $ids) | |
|
||
return "\"ID\" $value"; | ||
} | ||
|
||
/** | ||
* A ManyManyList defines functions such as getLocalKey, however on ManyManyThroughList | ||
* these functions are moved to ManyManyThroughQueryManipulator, but otherwise retain | ||
* the same signature. | ||
* | ||
* @param ManyManyList|ManyManyThroughList | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing variable name |
||
* | ||
* @return ManyManyList|ManyManyThroughQueryManipulator | ||
*/ | ||
protected function getManyManyInspector($list) | ||
{ | ||
$inspector = $list; | ||
if ($list instanceof ManyManyThroughList) { | ||
foreach ($list->dataQuery()->getDataQueryManipulators() as $manipulator) { | ||
if ($manipulator instanceof ManyManyThroughQueryManipulator) { | ||
$inspector = $manipulator; | ||
break; | ||
} | ||
} | ||
} | ||
return $inspector; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing return type