Skip to content
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

Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 133 additions & 44 deletions src/GridFieldOrderableRows.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Symbiote\GridFieldExtensions;

use Exception;
use SilverStripe\Control\Controller;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Core\ClassInfo;
Expand All @@ -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;
Expand Down Expand Up @@ -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
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing return type

protected function isManyMany(SS_List $list)
{
return $list instanceof ManyManyList || $list instanceof ManyManyThroughList;
}

/**
* Sets extra sort fields to apply before the sort field.
*
Expand Down Expand Up @@ -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());
Expand All @@ -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 . "'");
}

/**
Expand All @@ -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)
Expand Down Expand Up @@ -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);
}

Expand All @@ -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)
{
Expand Down Expand Up @@ -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) {
Expand All @@ -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.
Expand All @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if ->first() returns null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Safe due to earlier checks (short circuit returns) - which I'll also tighten up a touch.

]);
$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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if($isManyMany) is completely redundant here.

$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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the opposite.

Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If one is versioned and the other is not, throw an error.
I'll add a comment :)

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
Expand All @@ -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();
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -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)) {
Expand All @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$introspector or $inspector.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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 ? and : at the start of lines helps to indicate that it's part of a ternary condition and not just bad indentation

$key = $intropector->getLocalKey();
$foreignKey = $intropector->getForeignKey();
$foreignID = (int) $list->getForeignID();

if ($extra && array_key_exists($this->getSortField(), $extra)) {
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
}
}