NEW Add/remove callbacks on RelationList

This provides a mechanism for adjusting the behaviour of these
relations when building more complex data models.

For example the following example has a status field incorporates a
Status field into the relationship:

```php
function MyRelation() {
  $rel = $this->getManyManyComponents(‘MyRelation’);
  $rel = $rel->filter(‘Status’, ‘Active’);

  $rel->addCallbacks()->add(function ($relation, $item, $extra) {
    $item->Status = ‘Active’;
    $item->write();
  });
}
```

Introduces a new library dependency: http://github.com/sminnee/callbacklist
This commit is contained in:
Sam Minnee 2020-07-02 14:44:27 +12:00 committed by Ingo Schommer
parent ff18dec2e5
commit 0d7c5a9ece
8 changed files with 387 additions and 1 deletions

View File

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

View File

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

View File

@ -322,6 +322,10 @@ class ManyManyList extends RelationList
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");
}
return $this->removeByID($item->ID);
$result = $this->removeByID($item->ID);
return $result;
}
/**
@ -366,7 +372,13 @@ class ManyManyList extends RelationList
$query->addWhere([
"\"{$this->joinTable}\".\"{$this->localKey}\"" => $itemID
]);
// Perform the deletion
$query->execute();
if ($this->removeCallbacks) {
$this->removeCallbacks->call($this, [$itemID]);
}
}
/**
@ -403,7 +415,22 @@ class ManyManyList extends RelationList
$delete->addWhere([
"\"{$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();
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
$hasManyList = $this->manipulator->getParentRelationship($this->dataQuery());
$records = $hasManyList->filter($this->manipulator->getLocalKey(), $itemID);
$affectedIds = [];
// Rather than simple un-associating the record (as in has_many list)
// Delete the actual mapping row as many_many deletions behave.
/** @var DataObject $record */
foreach ($records as $record) {
$affectedIds[] = $record->ID;
$record->delete();
}
if ($this->removeCallbacks && $affectedIds) {
$this->removeCallbacks->call($this, $affectedIds);
}
}
public function removeAll()
{
// Empty has_many table matching the current foreign key
$hasManyList = $this->manipulator->getParentRelationship($this->dataQuery());
$affectedIds = $hasManyList->column('ID');
$hasManyList->removeAll();
if ($this->removeCallbacks && $affectedIds) {
$this->removeCallbacks->call($this, $affectedIds);
}
}
/**
@ -222,6 +233,10 @@ class ManyManyThroughList extends RelationList
if ($item instanceof DataObject) {
$item->setJoin($record, $this->manipulator->getJoinAlias());
}
if ($this->addCallbacks) {
$this->addCallbacks->call($this, $item, $extraFields);
}
}
/**

View File

@ -3,6 +3,7 @@
namespace SilverStripe\ORM;
use Exception;
use Sminnee\CallbackList\CallbackList;
/**
* A DataList that represents a relation.
@ -11,6 +12,79 @@ use Exception;
*/
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

View File

@ -3,6 +3,7 @@
namespace SilverStripe\ORM\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\Tests\DataObjectTest\Player;
use SilverStripe\ORM\Tests\DataObjectTest\Team;
use SilverStripe\ORM\Tests\HasManyListTest\Company;
use SilverStripe\ORM\Tests\HasManyListTest\CompanyCar;
@ -113,4 +114,79 @@ class HasManyListTest extends SapphireTest
['Model' => 'F40'],
], $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,
]);
}
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\ORM\DataObject;
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\PolyItem;
use SilverStripe\ORM\Tests\ManyManyThroughListTest\PolyJoinObject;
use SilverStripe\ORM\Tests\ManyManyThroughListTest\Locale;
use SilverStripe\ORM\Tests\ManyManyThroughListTest\FallbackLocale;
use SilverStripe\ORM\Tests\ManyManyThroughListTest\TestObject;
class ManyManyThroughListTest extends SapphireTest
{
@ -362,4 +365,95 @@ class ManyManyThroughListTest extends SapphireTest
$this->assertSame('International', $firstReverse->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));
}
}