Merge pull request #9572 from sminnee/pulls/manymanylist-add-callback

NEW: Add/remove callbacks for relation lists
This commit is contained in:
Ingo Schommer 2020-09-18 16:56:20 +12:00 committed by GitHub
commit ecb03562de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 387 additions and 1 deletions

View File

@ -35,6 +35,7 @@
"silverstripe/config": "^1@dev", "silverstripe/config": "^1@dev",
"silverstripe/assets": "^1@dev", "silverstripe/assets": "^1@dev",
"silverstripe/vendor-plugin": "^1.4", "silverstripe/vendor-plugin": "^1.4",
"sminnee/callbacklist": "^0.1",
"swiftmailer/swiftmailer": "~5.4", "swiftmailer/swiftmailer": "~5.4",
"symfony/cache": "^3.3@dev", "symfony/cache": "^3.3@dev",
"symfony/config": "^3.2", "symfony/config": "^3.2",

View File

@ -93,6 +93,10 @@ class HasManyList extends RelationList
$item->$foreignKey = $foreignID; $item->$foreignKey = $foreignID;
$item->write(); $item->write();
if ($this->addCallbacks) {
$this->addCallbacks->call($this, $item, []);
}
} }
/** /**
@ -136,5 +140,9 @@ class HasManyList extends RelationList
$item->$foreignKey = null; $item->$foreignKey = null;
$item->write(); $item->write();
} }
if ($this->removeCallbacks) {
$this->removeCallbacks->call($this, [$item->ID]);
}
} }
} }

View File

@ -322,6 +322,10 @@ class ManyManyList extends RelationList
DB::manipulate($manipulation); DB::manipulate($manipulation);
} }
if ($this->addCallbacks) {
$this->addCallbacks->call($this, $item, $extraFields);
}
} }
/** /**
@ -338,7 +342,9 @@ class ManyManyList extends RelationList
throw new InvalidArgumentException("ManyManyList::remove() expecting a $this->dataClass object"); throw new InvalidArgumentException("ManyManyList::remove() expecting a $this->dataClass object");
} }
return $this->removeByID($item->ID); $result = $this->removeByID($item->ID);
return $result;
} }
/** /**
@ -366,7 +372,13 @@ class ManyManyList extends RelationList
$query->addWhere([ $query->addWhere([
"\"{$this->joinTable}\".\"{$this->localKey}\"" => $itemID "\"{$this->joinTable}\".\"{$this->localKey}\"" => $itemID
]); ]);
// Perform the deletion
$query->execute(); $query->execute();
if ($this->removeCallbacks) {
$this->removeCallbacks->call($this, [$itemID]);
}
} }
/** /**
@ -403,7 +415,22 @@ class ManyManyList extends RelationList
$delete->addWhere([ $delete->addWhere([
"\"{$this->joinTable}\".\"{$this->localKey}\" IN ($subSelect)" => $parameters "\"{$this->joinTable}\".\"{$this->localKey}\" IN ($subSelect)" => $parameters
]); ]);
$affectedIds = [];
if ($this->removeCallbacks) {
$affectedIds = $delete
->toSelect()
->setSelect("\"{$this->joinTable}\".\"{$this->localKey}\"")
->execute()
->column();
}
// Perform the deletion
$delete->execute(); $delete->execute();
if ($this->removeCallbacks && $affectedIds) {
$this->removeCallbacks->call($this, $affectedIds);
}
} }
/** /**

View File

@ -130,20 +130,31 @@ class ManyManyThroughList extends RelationList
// Find has_many row with a local key matching the given id // Find has_many row with a local key matching the given id
$hasManyList = $this->manipulator->getParentRelationship($this->dataQuery()); $hasManyList = $this->manipulator->getParentRelationship($this->dataQuery());
$records = $hasManyList->filter($this->manipulator->getLocalKey(), $itemID); $records = $hasManyList->filter($this->manipulator->getLocalKey(), $itemID);
$affectedIds = [];
// Rather than simple un-associating the record (as in has_many list) // Rather than simple un-associating the record (as in has_many list)
// Delete the actual mapping row as many_many deletions behave. // Delete the actual mapping row as many_many deletions behave.
/** @var DataObject $record */ /** @var DataObject $record */
foreach ($records as $record) { foreach ($records as $record) {
$affectedIds[] = $record->ID;
$record->delete(); $record->delete();
} }
if ($this->removeCallbacks && $affectedIds) {
$this->removeCallbacks->call($this, $affectedIds);
}
} }
public function removeAll() public function removeAll()
{ {
// Empty has_many table matching the current foreign key // Empty has_many table matching the current foreign key
$hasManyList = $this->manipulator->getParentRelationship($this->dataQuery()); $hasManyList = $this->manipulator->getParentRelationship($this->dataQuery());
$affectedIds = $hasManyList->column('ID');
$hasManyList->removeAll(); $hasManyList->removeAll();
if ($this->removeCallbacks && $affectedIds) {
$this->removeCallbacks->call($this, $affectedIds);
}
} }
/** /**
@ -222,6 +233,10 @@ class ManyManyThroughList extends RelationList
if ($item instanceof DataObject) { if ($item instanceof DataObject) {
$item->setJoin($record, $this->manipulator->getJoinAlias()); $item->setJoin($record, $this->manipulator->getJoinAlias());
} }
if ($this->addCallbacks) {
$this->addCallbacks->call($this, $item, $extraFields);
}
} }
/** /**

View File

@ -3,6 +3,7 @@
namespace SilverStripe\ORM; namespace SilverStripe\ORM;
use Exception; use Exception;
use Sminnee\CallbackList\CallbackList;
/** /**
* A DataList that represents a relation. * A DataList that represents a relation.
@ -11,6 +12,79 @@ use Exception;
*/ */
abstract class RelationList extends DataList implements Relation abstract class RelationList extends DataList implements Relation
{ {
/**
* @var CallbackList|null
*/
protected $addCallbacks;
/**
* @var CallbackList|null
*/
protected $removeCallbacks;
/**
* Manage callbacks which are called after the add() action is completed.
* Each callback will be passed (RelationList $this, DataObject|int $item, array $extraFields).
* If a relation methods is manually defined, this can be called to adjust the behaviour
* when adding records to this list.
*
* Needs to be defined through an overloaded relationship getter
* to ensure it is set consistently. These getters return a new object
* every time they're called.
*
* Note that subclasses of RelationList must implement the callback for it to function
*
* @return this
*/
public function addCallbacks(): CallbackList
{
if (!$this->addCallbacks) {
$this->addCallbacks = new CallbackList();
}
return $this->addCallbacks;
}
/**
* Manage callbacks which are called after the remove() action is completed.
* Each Callback will be passed (RelationList $this, [int] $removedIds).
*
* Needs to be defined through an overloaded relationship getter
* to ensure it is set consistently. These getters return a new object
* every time they're called. Example:
*
* ```php
* class MyObject extends DataObject()
* {
* private static $many_many = [
* 'MyRelationship' => '...',
* ];
* public function MyRelationship()
* {
* $list = $this->getManyManyComponents('MyRelationship');
* $list->removeCallbacks()->add(function ($removedIds) {
* // ...
* });
* return $list;
* }
* }
* ```
*
* If a relation methods is manually defined, this can be called to adjust the behaviour
* when adding records to this list.
*
* Subclasses of RelationList must implement the callback for it to function
*
* @return this
*/
public function removeCallbacks(): CallbackList
{
if (!$this->removeCallbacks) {
$this->removeCallbacks = new CallbackList();
}
return $this->removeCallbacks;
}
/** /**
* Any number of foreign keys to apply to this list * Any number of foreign keys to apply to this list

View File

@ -3,6 +3,7 @@
namespace SilverStripe\ORM\Tests; namespace SilverStripe\ORM\Tests;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\Tests\DataObjectTest\Player;
use SilverStripe\ORM\Tests\DataObjectTest\Team; use SilverStripe\ORM\Tests\DataObjectTest\Team;
use SilverStripe\ORM\Tests\HasManyListTest\Company; use SilverStripe\ORM\Tests\HasManyListTest\Company;
use SilverStripe\ORM\Tests\HasManyListTest\CompanyCar; use SilverStripe\ORM\Tests\HasManyListTest\CompanyCar;
@ -113,4 +114,79 @@ class HasManyListTest extends SapphireTest
['Model' => 'F40'], ['Model' => 'F40'],
], $company->CompanyCars()->sort('"Model" ASC')); ], $company->CompanyCars()->sort('"Model" ASC'));
} }
public function testCallbackOnSetById()
{
$addedIds = [];
$removedIds = [];
$base = $this->objFromFixture(Company::class, 'silverstripe');
$relation = $base->Employees();
$remove = $relation->First();
$add = new Employee();
$add->write();
$relation->addCallbacks()->add(function ($list, $item) use (&$addedIds) {
$addedIds[] = $item;
});
$relation->removeCallbacks()->add(function ($list, $ids) use (&$removedIds) {
$removedIds = $ids;
});
$relation->setByIDList(array_merge(
$base->Employees()->exclude('ID', $remove->ID)->column('ID'),
[$add->ID]
));
$this->assertEquals([$remove->ID], $removedIds);
}
public function testAddCallback()
{
$added = [];
$base = $this->objFromFixture(Company::class, 'silverstripe');
$relation = $base->Employees();
$add = new Employee();
$add->write();
$relation->addCallbacks()->add(function ($list, $item) use (&$added) {
$added[] = $item;
});
$relation->add($add);
$this->assertEquals([$add], $added);
}
public function testRemoveCallbackOnRemove()
{
$removedIds = [];
$base = $this->objFromFixture(Company::class, 'silverstripe');
$relation = $base->Employees();
$remove = $relation->First();
$relation->removeCallbacks()->add(function ($list, $ids) use (&$removedIds) {
$removedIds = $ids;
});
$relation->remove($remove);
$this->assertEquals([$remove->ID], $removedIds);
}
public function testRemoveCallbackOnRemoveById()
{
$removedIds = [];
$base = $this->objFromFixture(Company::class, 'silverstripe');
$relation = $base->Employees();
$remove = $relation->First();
$relation->removeCallbacks()->add(function ($list, $ids) use (&$removedIds) {
$removedIds = $ids;
});
$relation->removeByID($remove->ID);
$this->assertEquals([$remove->ID], $removedIds);
}
} }

View File

@ -457,4 +457,95 @@ class ManyManyListTest extends SapphireTest
'ManyManyDynamicField' => false, 'ManyManyDynamicField' => false,
]); ]);
} }
public function testCallbackOnSetById()
{
$addedIds = [];
$removedIds = [];
$base = $this->objFromFixture(Team::class, 'team1');
$relation = $base->Players();
$remove = $relation->First();
$add = new Player();
$add->write();
$relation->addCallbacks()->add(function ($list, $item, $extraFields) use (&$removedIds) {
$addedIds[] = $item;
});
$relation->removeCallbacks()->add(function ($list, $ids) use (&$removedIds) {
$removedIds = $ids;
});
$relation->setByIDList(array_merge(
$base->Players()->exclude('ID', $remove->ID)->column('ID'),
[$add->ID]
));
$this->assertEquals([$remove->ID], $removedIds);
}
public function testAddCallbackWithExtraFields()
{
$added = [];
$base = $this->objFromFixture(Team::class, 'team1');
$relation = $base->Players();
$add = new Player();
$add->write();
$relation->addCallbacks()->add(function ($list, $item, $extraFields) use (&$added) {
$added[] = [$item, $extraFields];
});
$relation->add($add, ['Position' => 'Quarterback']);
$this->assertEquals([[$add, ['Position' => 'Quarterback']]], $added);
}
public function testRemoveCallbackOnRemove()
{
$removedIds = [];
$base = $this->objFromFixture(Team::class, 'team1');
$relation = $base->Players();
$remove = $relation->First();
$relation->removeCallbacks()->add(function ($list, $ids) use (&$removedIds) {
$removedIds = $ids;
});
$relation->remove($remove);
$this->assertEquals([$remove->ID], $removedIds);
}
public function testRemoveCallbackOnRemoveById()
{
$removedIds = [];
$base = $this->objFromFixture(Team::class, 'team1');
$relation = $base->Players();
$remove = $relation->First();
$relation->removeCallbacks()->add(function ($list, $ids) use (&$removedIds) {
$removedIds = $ids;
});
$relation->removeByID($remove->ID);
$this->assertEquals([$remove->ID], $removedIds);
}
public function testRemoveCallbackOnRemoveAll()
{
$removedIds = [];
$base = $this->objFromFixture(Team::class, 'team1');
$relation = $base->Players();
$remove = $relation->column('ID');
$relation->removeCallbacks()->add(function ($list, $ids) use (&$removedIds) {
$removedIds = $ids;
});
$relation->removeAll();
$this->assertEquals(sort($remove), sort($removedIds));
}
} }

View File

@ -6,11 +6,14 @@ use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ManyManyThroughList; use SilverStripe\ORM\ManyManyThroughList;
use SilverStripe\ORM\Tests\DataObjectTest\Player;
use SilverStripe\ORM\Tests\DataObjectTest\Team;
use SilverStripe\ORM\Tests\ManyManyThroughListTest\Item; use SilverStripe\ORM\Tests\ManyManyThroughListTest\Item;
use SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyItem; use SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyItem;
use SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyJoinObject; use SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyJoinObject;
use SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale; use SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale;
use SilverStripe\ORM\Tests\ManyManyThroughListTest\FallbackLocale; use SilverStripe\ORM\Tests\ManyManyThroughListTest\FallbackLocale;
use SilverStripe\ORM\Tests\ManyManyThroughListTest\TestObject;
class ManyManyThroughListTest extends SapphireTest class ManyManyThroughListTest extends SapphireTest
{ {
@ -362,4 +365,95 @@ class ManyManyThroughListTest extends SapphireTest
$this->assertSame('International', $firstReverse->Title); $this->assertSame('International', $firstReverse->Title);
$this->assertSame('Argentina', $secondReverse->Title); $this->assertSame('Argentina', $secondReverse->Title);
} }
public function testCallbackOnSetById()
{
$addedIds = [];
$removedIds = [];
$base = $this->objFromFixture(ManyManyThroughListTest\TestObject::class, 'parent1');
$relation = $base->Items();
$remove = $relation->First();
$add = new Item();
$add->write();
$relation->addCallbacks()->add(function ($list, $item, $extraFields) use (&$removedIds) {
$addedIds[] = $item;
});
$relation->removeCallbacks()->add(function ($list, $ids) use (&$removedIds) {
$removedIds = $ids;
});
$relation->setByIDList(array_merge(
$base->Items()->exclude('ID', $remove->ID)->column('ID'),
[$add->ID]
));
$this->assertEquals([$remove->ID], $removedIds);
}
public function testAddCallbackWithExtraFields()
{
$added = [];
$base = $this->objFromFixture(ManyManyThroughListTest\TestObject::class, 'parent1');
$relation = $base->Items();
$add = new Item();
$add->write();
$relation->addCallbacks()->add(function ($list, $item, $extraFields) use (&$added) {
$added[] = [$item, $extraFields];
});
$relation->add($add, ['Sort' => '99']);
$this->assertEquals([[$add, ['Sort' => '99']]], $added);
}
public function testRemoveCallbackOnRemove()
{
$removedIds = [];
$base = $this->objFromFixture(ManyManyThroughListTest\TestObject::class, 'parent1');
$relation = $base->Items();
$remove = $relation->First();
$relation->removeCallbacks()->add(function ($list, $ids) use (&$removedIds) {
$removedIds = $ids;
});
$relation->remove($remove);
$this->assertEquals([$remove->ID], $removedIds);
}
public function testRemoveCallbackOnRemoveById()
{
$removedIds = [];
$base = $this->objFromFixture(ManyManyThroughListTest\TestObject::class, 'parent1');
$relation = $base->Items();
$remove = $relation->First();
$relation->removeCallbacks()->add(function ($list, $ids) use (&$removedIds) {
$removedIds = $ids;
});
$relation->removeByID($remove->ID);
$this->assertEquals([$remove->ID], $removedIds);
}
public function testRemoveCallbackOnRemoveAll()
{
$removedIds = [];
$base = $this->objFromFixture(ManyManyThroughListTest\TestObject::class, 'parent1');
$relation = $base->Items();
$remove = $relation->column('ID');
$relation->removeCallbacks()->add(function ($list, $ids) use (&$removedIds) {
$removedIds = $ids;
});
$relation->removeAll();
$this->assertEquals(sort($remove), sort($removedIds));
}
} }