Skip to content

Commit

Permalink
NEW Add a trait to dynamically generate edit URLs
Browse files Browse the repository at this point in the history
  • Loading branch information
GuySartorelli committed Sep 14, 2022
1 parent 36fd3f4 commit abc3fe5
Show file tree
Hide file tree
Showing 8 changed files with 401 additions and 0 deletions.
146 changes: 146 additions & 0 deletions code/HasCanonicalEditLink.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

namespace SilverStripe\Admin;

use LogicException;
use SilverStripe\CMS\Controllers\CMSMain;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\Forms\GridField\GridFieldDetailForm;
use SilverStripe\ORM\DataObject;

/**
* A trait that automatically generates a CMS edit link for DataObjects even if
* they are canonically edited in some nested {@link GridField}.
* Designed to be used in conjunction with the {@link CMSPreviewable} interface.
*
* For nested relations (e.g. a DataObject managed in a GridField of another DataObject)
* you can apply this trait to both the parent and the child object and the links
* will chain down the nested `GridField`s to the root canonical edit owner.
*
* You must set a canonical_edit_owner config variable which defines the canonical
* owner for this class.
* e.g. set this to a {@link LeftAndMain} class:
* private static string canonical_edit_owner = MyModelAdmin::class;
* or to a has_one relation:
* private static string canonical_edit_owner = 'Parent';
*
* Note that the canonical edit owner must implement a getEditLinkForObject() method.
*
* If the canonical edit owner is a has_one relation, the class on the other end
* of the relation must have a CMSEditLink() method.
*/
trait HasCanonicalEditLink
{
/**
* Get the admin or DataObject which owns this object for CMS editing purposes.
*
* @return LeftAndMain|DataObject|null
*/
public function getCanonicalEditOwner()
{
$ownerType = static::config()->get('canonical_edit_owner');
if (is_subclass_of($ownerType, LeftAndMain::class)) {
return $ownerType::singleton();
}
return $this->getComponent($ownerType);
}

/**
* Get the link for editing an object from the CMS edit form of this object.
* @throws LogicException if a link cannot be established
* e.g. if the object is not in a has_many relation or not edited inside a GridField.
*/
public function getEditLinkForObject(DataObject $obj, string $reciprocalRelation): string
{
$fields = $this->getCMSFields();
$link = $this->getLinkForRelation($this->hasMany(false), $obj, $reciprocalRelation, $fields);
if (!$link) {
throw new LogicException('Could not produce an edit link for the passed object.');
}
return $link;
}

/**
* Get a link to edit this page in the CMS.
*/
public function CMSEditLink(): string
{
$owner = $this->getCanonicalEditOwner();
if (!$owner || !$owner->exists()) {
return '';
}

if (!$owner->hasMethod('getEditLinkForObject')) {
throw new LogicException('The canonical owner must implement getEditLinkForObject()');
}

if ($owner instanceof DataObject) {
$relativeLink = $owner->getEditLinkForObject($this, static::config()->get('canonical_edit_owner'));
} else {
$relativeLink = $owner->getEditLinkForObject($this);
}
return Director::absoluteURL($relativeLink);
}

private function getLinkForRelation(array $componentConfig, DataObject $obj, string $reciprocalRelation, FieldList $fields): string
{
$candidate = null;
foreach ($componentConfig as $relation => $class) {
// Check for dot notation being used to explicitly mark the reciprocal relation.
$remoteField = null;
if (strpos($class ?? '', '.') !== false) {
list($class, $remoteField) = explode('.', $class ?? '');
}

// We're only interested in relations to the $obj class.
if (!is_a($obj, $class)) {
continue;
}

if ($remoteField) {
if ($remoteField === $reciprocalRelation) {
// We've found a direct reciprocal relation, so this is definitely correct.
if ($this->relationIsEditable($relation, $fields)) {
return $this->constructLink($relation, $obj->ID);
}
// If the relation isn't in a gridfield, we have no link for it.
return '';
}
// We're not interested in unrelated relations.
continue;
}

// Check for relations that have gridfields we can build a link from.
if ($this->relationIsEditable($relation, $fields)) {
$candidate = $relation;
}
}

// Only do this if we didn't find a direct reciprocal relation.
return $candidate ? $this->constructLink($candidate, $obj->ID) : '';
}

private function relationIsEditable(string $relation, FieldList $fields): bool
{
$field = $fields->dataFieldByName($relation);
return $field
&& $field instanceof GridField
&& $field->getConfig()->getComponentByType(GridFieldDetailForm::class);
}

private function constructLink(string $relation, int $id): string
{
$ownerType = static::config()->get('canonical_edit_owner');
$prefix = is_a($ownerType, CMSMain::class, true) ? 'field' : 'ItemEditForm/field';
return Controller::join_links(
$this->CMSEditLink(),
$prefix,
$relation,
'item',
$id
);
}
}
98 changes: 98 additions & 0 deletions tests/php/HasCanonicalEditLinkTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

namespace SilverStripe\Admin\Tests;

use LogicException;
use SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\BasicNestedObject;
use SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\BelongsToModelAdmin;
use SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\CanonicalModelAdmin;
use SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\NestedObject;
use SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\PolymorphicNestedObject;
use SilverStripe\Dev\SapphireTest;

class HasCanonicalEditLinkTest extends SapphireTest
{
protected static $fixture_file = 'HasCanonicalEditLinkTest.yml';

protected $usesDatabase = true;

protected static $extra_dataobjects = [
BelongsToModelAdmin::class,
BasicNestedObject::class,
NestedObject::class,
PolymorphicNestedObject::class,
];

protected static $extra_controllers = [
CanonicalModelAdmin::class,
];

public function testGetCanonicalEditOwner()
{
$adminSingleton = CanonicalModelAdmin::singleton();
$root = $this->objFromFixture(BelongsToModelAdmin::class, 'root');
$basicNested = $this->objFromFixture(BasicNestedObject::class, 'one');
$nested = $this->objFromFixture(NestedObject::class, 'one');
$polymorphic = $this->objFromFixture(PolymorphicNestedObject::class, 'one');

$this->assertSame($adminSingleton, $root->getCanonicalEditOwner());
$this->assertSame($root->ID, $basicNested->getCanonicalEditOwner()->ID);
$this->assertSame($root->ID, $nested->getCanonicalEditOwner()->ID);
$this->assertSame($root->ID, $polymorphic->getCanonicalEditOwner()->ID);
}

public function testGetEditLinkForObject()
{
$root = $this->objFromFixture(BelongsToModelAdmin::class, 'root');
$basicNested = $this->objFromFixture(BasicNestedObject::class, 'one');
$nested = $this->objFromFixture(NestedObject::class, 'one');
$polymorphic = $this->objFromFixture(PolymorphicNestedObject::class, 'one');

$this->assertSame(
"http://localhost/admin/canonical-test/belongsHere/EditForm/field/belongsHere/item/$root->ID/ItemEditForm/field/BasicNested/item/$basicNested->ID",
$root->getEditLinkForObject($basicNested, 'Parent')
);
$this->assertSame(
"http://localhost/admin/canonical-test/belongsHere/EditForm/field/belongsHere/item/$root->ID/ItemEditForm/field/Nested/item/$nested->ID",
$root->getEditLinkForObject($nested, 'Parent')
);
$this->assertSame(
"http://localhost/admin/canonical-test/belongsHere/EditForm/field/belongsHere/item/$root->ID/ItemEditForm/field/Polymorphic/item/$polymorphic->ID",
$root->getEditLinkForObject($polymorphic, 'Parent')
);
}

public function testGetEditLinkForObjectException()
{
$root = $this->objFromFixture(BelongsToModelAdmin::class, 'root');
$nested = $this->objFromFixture(NestedObject::class, 'redHerringOne');

$this->expectException(LogicException::class);
$this->assertNull($root->getEditLinkForObject($nested, 'AnotherOfTheSameClass'));
}

public function testCMSEditLink()
{
$root = $this->objFromFixture(BelongsToModelAdmin::class, 'root');
$basicNested = $this->objFromFixture(BasicNestedObject::class, 'one');
$nested = $this->objFromFixture(NestedObject::class, 'one');
$polymorphic = $this->objFromFixture(PolymorphicNestedObject::class, 'one');

$this->assertSame(
"http://localhost/admin/canonical-test/belongsHere/EditForm/field/belongsHere/item/$root->ID",
$root->CMSEditLink()
);
$this->assertSame(
"http://localhost/admin/canonical-test/belongsHere/EditForm/field/belongsHere/item/$root->ID/ItemEditForm/field/BasicNested/item/$basicNested->ID",
$basicNested->CMSEditLink()
);
$this->assertSame(
"http://localhost/admin/canonical-test/belongsHere/EditForm/field/belongsHere/item/$root->ID/ItemEditForm/field/Nested/item/$nested->ID",
$nested->CMSEditLink()
);
$this->assertSame(
"http://localhost/admin/canonical-test/belongsHere/EditForm/field/belongsHere/item/$root->ID/ItemEditForm/field/Polymorphic/item/$polymorphic->ID",
$polymorphic->CMSEditLink()
);
}
}
33 changes: 33 additions & 0 deletions tests/php/HasCanonicalEditLinkTest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\BasicNestedObject:
one:
Name: 'some name'

SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\PolymorphicNestedObject:
one:
Name: 'some name'

SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\NestedObject:
one:
Name: 'some name'
redHerringOne:
Name: 'This exists so there is a record in an edge-case relation'
redHerringTwo:
Name: 'This exists so there is a record in an edge-case relation'

SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\BelongsToModelAdmin:
redHerringOne:
Name: 'This exists so we know it doesnt just grab the first record'
root:
Name: 'this is the record we care about'
Nested:
- '=>SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\NestedObject.one'
ArbitraryRelation:
- '=>SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\NestedObject.redHerringOne'
AnotherArbitraryRelation:
- '=>SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\NestedObject.redHerringTwo'
BasicNested:
- '=>SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\BasicNestedObject.one'
PolyMorphic:
- '=>SilverStripe\Admin\Tests\HasCanonicalEditLinkTest\PolymorphicNestedObject.one'
redHerringTwo:
Name: 'This exists so we know it doesnt just grab the last record'
24 changes: 24 additions & 0 deletions tests/php/HasCanonicalEditLinkTest/BasicNestedObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace SilverStripe\Admin\Tests\HasCanonicalEditLinkTest;

use SilverStripe\Admin\HasCanonicalEditLink;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;

class BasicNestedObject extends DataObject implements TestOnly
{
use HasCanonicalEditLink;

private static $table_name = 'HasCanonicalEditLinkTest_BasicNestedObject';

private static $canonical_edit_owner = 'Parent';

private static $db = [
'Name' => 'Varchar(25)',
];

private static $has_one = [
'Parent' => BelongsToModelAdmin::class,
];
}
35 changes: 35 additions & 0 deletions tests/php/HasCanonicalEditLinkTest/BelongsToModelAdmin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace SilverStripe\Admin\Tests\HasCanonicalEditLinkTest;

use SilverStripe\Admin\HasCanonicalEditLink;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;

class BelongsToModelAdmin extends DataObject implements TestOnly
{
use HasCanonicalEditLink;

private static $table_name = 'HasCanonicalEditLinkTest_BelongsToModelAdmin';

private static $canonical_edit_owner = CanonicalModelAdmin::class;

private static $db = [
'Name' => 'Varchar(25)',
];

private static $has_many = [
'ArbitraryRelation' => NestedObject::class,
'Nested' => NestedObject::class . '.Parent',
'AnotherArbitraryRelation' => NestedObject::class . '.AnotherOfTheSameClass',
'BasicNested' => BasicNestedObject::class,
'PolyMorphic' => PolymorphicNestedObject::class,
];

public function getCMSFields()
{
$fields = parent::getCMSFields();
$fields->removeByName(['ArbitraryRelation', 'AnotherArbitraryRelation']);
return $fields;
}
}
15 changes: 15 additions & 0 deletions tests/php/HasCanonicalEditLinkTest/CanonicalModelAdmin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace SilverStripe\Admin\Tests\HasCanonicalEditLinkTest;

use SilverStripe\Admin\ModelAdmin;
use SilverStripe\Dev\TestOnly;

class CanonicalModelAdmin extends ModelAdmin implements TestOnly
{
private static $url_segment = 'canonical-test';

private static $managed_models = [
'belongsHere' => BelongsToModelAdmin::class,
];
}
26 changes: 26 additions & 0 deletions tests/php/HasCanonicalEditLinkTest/NestedObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace SilverStripe\Admin\Tests\HasCanonicalEditLinkTest;

use SilverStripe\Admin\HasCanonicalEditLink;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;

class NestedObject extends DataObject implements TestOnly
{
use HasCanonicalEditLink;

private static $table_name = 'HasCanonicalEditLinkTest_NestedObject';

private static $canonical_edit_owner = 'Parent';

private static $db = [
'Name' => 'Varchar(25)',
];

private static $has_one = [
'Parent' => BelongsToModelAdmin::class,
'AnotherOfTheSameClass' => BelongsToModelAdmin::class,
'ThirdOne' => BelongsToModelAdmin::class,
];
}
Loading

0 comments on commit abc3fe5

Please sign in to comment.