Skip to content

Commit

Permalink
NEW Add an extension to dynamically generate edit URLs
Browse files Browse the repository at this point in the history
  • Loading branch information
GuySartorelli committed Sep 29, 2022
1 parent fd91da3 commit 052ed99
Show file tree
Hide file tree
Showing 10 changed files with 417 additions and 6 deletions.
149 changes: 149 additions & 0 deletions code/CMSEditLinkExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

namespace SilverStripe\Admin;

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

/**
* An extension 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 extension to both the parent and the child object and the links
* will chain down the nested `GridField`s to the root cms edit owner.
*
* You must set a cms_edit_owner config variable which defines the cms edit
* owner for this class.
* e.g. set this to a {@link LeftAndMain} class:
* private static string cms_edit_owner = MyModelAdmin::class;
* or to a has_one relation:
* private static string cms_edit_owner = 'Parent';
*
* Note that the cms edit owner must implement a getCMSEditLinkForManagedDataObject() method.
*
* If the cms edit owner is a has_one relation, the class on the other end
* of the relation must have a CMSEditLink() method.
*/
class CMSEditLinkExtension extends Extension
{
private static string $cms_edit_owner = '';

/**
* Get the admin or DataObject which owns this object for CMS editing purposes.
*
* @return LeftAndMain|DataObject|null
*/
public function getCMSEditOwner()
{
$ownerType = $this->owner->config()->get('cms_edit_owner');
if (is_subclass_of($ownerType, LeftAndMain::class)) {
return $ownerType::singleton();
}
return $this->owner->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 getCMSEditLinkForManagedDataObject(DataObject $obj, string $reciprocalRelation): string
{
$fields = $this->owner->getCMSFields();
$link = $this->getCMSEditLinkForRelation($this->owner->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 DataObject in the CMS.
*/
public function CMSEditLink(): string
{
$owner = $this->owner->getCMSEditOwner();
if (!$owner || !$owner->exists()) {
return '';
}

if (!$owner->hasMethod('getCMSEditLinkForManagedDataObject')) {
throw new LogicException('The cms edit owner must implement getCMSEditLinkForManagedDataObject()');
}

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

private function getCMSEditLinkForRelation(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 = $this->owner->config()->get('cms_edit_owner');
$prefix = is_a($ownerType, CMSMain::class, true) ? 'field' : 'ItemEditForm/field';
return Controller::join_links(
$this->owner->CMSEditLink(),
$prefix,
$relation,
'item',
$id
);
}
}
4 changes: 2 additions & 2 deletions code/ModelAdmin.php
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ public function getLinkForModelTab(string $modelTab): string
*
* @throws InvalidArgumentException if $obj is not managed by this ModelAdmin.
*/
public function getEditLinkForManagedDataObject(DataObject $obj): string
public function getCMSEditLinkForManagedDataObject(DataObject $obj): string
{
$modelTab = $this->getModelTabForModelClass($obj->ClassName);
if ($modelTab === null) {
Expand Down Expand Up @@ -450,7 +450,7 @@ public function SearchForm()
* }
* </code>
*
* Note: If you override this method you may also need to override getEditLinkForManagedDataObject()
* Note: If you override this method you may also need to override getCMSEditLinkForManagedDataObject()
*
* @return \SilverStripe\ORM\DataList
*/
Expand Down
97 changes: 97 additions & 0 deletions tests/php/CMSEditLinkExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

namespace SilverStripe\Admin\Tests;

use LogicException;
use SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\BasicNestedObject;
use SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\BelongsToModelAdmin;
use SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\CMSEditModelAdmin;
use SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\NestedObject;
use SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\PolymorphicNestedObject;
use SilverStripe\Dev\SapphireTest;

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

protected $usesDatabase = true;

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

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

public function testGetCMSEditOwner()
{
$adminSingleton = CMSEditModelAdmin::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->getCMSEditOwner());
$this->assertSame($root->ID, $basicNested->getCMSEditOwner()->ID);
$this->assertSame($root->ID, $nested->getCMSEditOwner()->ID);
$this->assertSame($root->ID, $polymorphic->getCMSEditOwner()->ID);
}

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

$rootUrl = "http://localhost/admin/cms-edit-test/belongsHere/EditForm/field/belongsHere/item/$root->ID";
$this->assertSame(
"$rootUrl/ItemEditForm/field/BasicNested/item/$basicNested->ID",
$root->getCMSEditLinkForManagedDataObject($basicNested, 'Parent')
);
$this->assertSame(
"$rootUrl/ItemEditForm/field/Nested/item/$nested->ID",
$root->getCMSEditLinkForManagedDataObject($nested, 'Parent')
);
$this->assertSame(
"$rootUrl/ItemEditForm/field/PolyMorphic/item/$polymorphic->ID",
$root->getCMSEditLinkForManagedDataObject($polymorphic, 'Parent')
);
}

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

$this->expectException(LogicException::class);
$this->assertNull($root->getCMSEditLinkForManagedDataObject($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');

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

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

SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\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\CMSEditLinkExtensionTest\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\CMSEditLinkExtensionTest\NestedObject.one'
ArbitraryRelation:
- '=>SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\NestedObject.redHerringOne'
AnotherArbitraryRelation:
- '=>SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\NestedObject.redHerringTwo'
BasicNested:
- '=>SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\BasicNestedObject.one'
PolyMorphic:
- '=>SilverStripe\Admin\Tests\CMSEditLinkExtensionTest\PolymorphicNestedObject.one'
redHerringTwo:
Name: 'This exists so we know it doesnt just grab the last record'
26 changes: 26 additions & 0 deletions tests/php/CMSEditLinkExtensionTest/BasicNestedObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace SilverStripe\Admin\Tests\CMSEditLinkExtensionTest;

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

class BasicNestedObject extends DataObject implements TestOnly
{
private static $table_name = 'CMSEditLinkTest_BasicNestedObject';

private static $cms_edit_owner = 'Parent';

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

private static $has_one = [
'Parent' => BelongsToModelAdmin::class,
];

private static $extensions = [
CMSEditLinkExtension::class,
];
}
37 changes: 37 additions & 0 deletions tests/php/CMSEditLinkExtensionTest/BelongsToModelAdmin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace SilverStripe\Admin\Tests\CMSEditLinkExtensionTest;

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

class BelongsToModelAdmin extends DataObject implements TestOnly
{
private static $table_name = 'CMSEditLinkTest_BelongsToModelAdmin';

private static $cms_edit_owner = CMSEditModelAdmin::class;

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

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

private static $extensions = [
CMSEditLinkExtension::class,
];
}
15 changes: 15 additions & 0 deletions tests/php/CMSEditLinkExtensionTest/CMSEditModelAdmin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace SilverStripe\Admin\Tests\CMSEditLinkExtensionTest;

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

class CMSEditModelAdmin extends ModelAdmin implements TestOnly
{
private static $url_segment = 'cms-edit-test';

private static $managed_models = [
'belongsHere' => BelongsToModelAdmin::class,
];
}
Loading

0 comments on commit 052ed99

Please sign in to comment.