NEW Added beforeExtending, afterExtending, and beforeUpdateCMSFields to allow user code better control over interaction with extending methods

This commit is contained in:
Damian Mooyman 2013-05-16 10:34:45 +12:00
parent 9f532fe976
commit 6e0e3564e1
6 changed files with 276 additions and 0 deletions

View File

@ -66,6 +66,52 @@ abstract class Object {
*/
protected $extension_instances = array();
/**
* List of callbacks to call prior to extensions having extend called on them,
* each grouped by methodName.
*
* @var array[callable]
*/
protected $beforeExtendCallbacks = array();
/**
* Allows user code to hook into Object::extend prior to control
* being delegated to extensions. Each callback will be reset
* once called.
*
* @param string $method The name of the method to hook into
* @param callable $callback The callback to execute
*/
protected function beforeExtending($method, $callback) {
if(empty($this->beforeExtendCallbacks[$method])) {
$this->beforeExtendCallbacks[$method] = array();
}
$this->beforeExtendCallbacks[$method][] = $callback;
}
/**
* List of callbacks to call after extensions having extend called on them,
* each grouped by methodName.
*
* @var array[callable]
*/
protected $afterExtendCallbacks = array();
/**
* Allows user code to hook into Object::extend after control
* being delegated to extensions. Each callback will be reset
* once called.
*
* @param string $method The name of the method to hook into
* @param callable $callback The callback to execute
*/
protected function afterExtending($method, $callback) {
if(empty($this->afterExtendCallbacks[$method])) {
$this->afterExtendCallbacks[$method] = array();
}
$this->afterExtendCallbacks[$method][] = $callback;
}
/**
* An implementation of the factory method, allows you to create an instance of a class
*
@ -929,6 +975,14 @@ abstract class Object {
public function extend($method, &$a1=null, &$a2=null, &$a3=null, &$a4=null, &$a5=null, &$a6=null, &$a7=null) {
$values = array();
if(!empty($this->beforeExtendCallbacks[$method])) {
foreach(array_reverse($this->beforeExtendCallbacks[$method]) as $callback) {
$value = call_user_func($callback, $a1, $a2, $a3, $a4, $a5, $a6, $a7);
if($value !== null) $values[] = $value;
}
$this->beforeExtendCallbacks[$method] = array();
}
if($this->extension_instances) foreach($this->extension_instances as $instance) {
if(method_exists($instance, $method)) {
$instance->setOwner($this);
@ -938,6 +992,14 @@ abstract class Object {
}
}
if(!empty($this->afterExtendCallbacks[$method])) {
foreach(array_reverse($this->afterExtendCallbacks[$method]) as $callback) {
$value = call_user_func($callback, $a1, $a2, $a3, $a4, $a5, $a6, $a7);
if($value !== null) $values[] = $value;
}
$this->afterExtendCallbacks[$method] = array();
}
return $values;
}

View File

@ -455,3 +455,6 @@ you can enable those warnings and future-proof your code already.
* Hard limit displayed pages in the CMS tree to `500`, and the number of direct children to `250`,
to avoid excessive resource usage. Configure through `Hierarchy.node_threshold_total` and `
Hierarchy.node_threshold_leaf`. Set to `0` to show tree unrestricted.
* `Object` now has `beforeExtending` and `afterExtending` to inject behaviour around method extension.
`DataObject` also has `beforeUpdateCMSFields` to insert fields between automatic scaffolding and extension
by `updateCMSFields`. See the [DataExtension Reference](/reference/dataextension) for more information.

View File

@ -78,6 +78,57 @@ The `$`fields parameter is passed by reference, as it is an object.
$fields->push(new UploadField('Image', 'Profile Image'));
}
### Adding/modifying fields prior to extensions
User code can intervene in the process of extending cms fields by using `beforeUpdateCMSFields`
in its implementation of `getCMSFields`. This can be useful in cases where user code will add
fields to a dataobject that should be present in the `$fields` parameter when passed to
`updateCMSFields` in extensions.
This method is preferred to disabling, enabling, and calling cms field extensions manually.
:::php
function getCMSFields() {
$this->beforeUpdateCMSFields(function($fields) {
// Include field which must be present when updateCMSFields is called on extensions
$fields->addFieldToTab("Root.Main", new TextField('Detail', 'Details', null, 255));
});
$fields = parent::getCMSFields();
// ... additional fields here
return $fields;
}
### Object extension injection points
`Object` now has two additional methods, `beforeExtending` and `afterExtending`, each of which takes a
method name and a callback to be executed immediately before and after `Object::extend()` is called on
extensions.
This is useful in many cases where working with modules such as `Translatable` which operate on
`DataObject` fields that must exist in the `FieldList` at the time that `$this->extend('UpdateCMSFields')`
is called.
<div class="notice" markdown='1'>
Please note that each callback is only ever called once, and then cleared, so multiple extensions
to the same function require that a callback is registered each time, if necessary.
</div>
Example: A class that wants to control default values during object initialisation. The code
needs to assign a value if not specified in self::$defaults, but before extensions have been called:
:::php
function __construct() {
$self = $this;
$this->beforeExtending('populateDefaults', function() uses ($self) {
if(empty($self->MyField)) {
$self->MyField = 'Value we want as a default if not specified in $defaults, but set before extensions';
}
});
parent::__construct();
}
### Custom database generation
Some extensions are designed to transparently add more sophisticated data-collection capabilities to your data object.

View File

@ -63,6 +63,8 @@ data management interfaces with very little custom coding.
You can also alter the fields of built-in and module `DataObject` classes through
your own `[DataExtension](/reference/dataextension)`, and a call to `[api:DataExtension->updateCMSFields()]`.
`[api::DataObject->beforeUpdateCMSFields()]` can also be used to interact with and add to automatically
scaffolded fields prior to being passed to extensions (See `[DataExtension](/reference/dataextension)`).
### Searchable Fields

View File

@ -1999,6 +1999,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $fs->getFieldList();
}
/**
* Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
* being called on extensions
*
* @param callable $callback The callback to execute
*/
protected function beforeUpdateCMSFields($callback) {
$this->beforeExtending('updateCMSFields', $callback);
}
/**
* Centerpiece of every data administration interface in Silverstripe,
* which returns a {@link FieldList} suitable for a {@link Form} object.

View File

@ -8,6 +8,9 @@ class DataExtensionTest extends SapphireTest {
'DataExtensionTest_Player',
'DataExtensionTest_RelatedObject',
'DataExtensionTest_MyObject',
'DataExtensionTest_CMSFieldsBase',
'DataExtensionTest_CMSFieldsChild',
'DataExtensionTest_CMSFieldsGrandchild'
);
protected $requiredExtensions = array(
@ -156,6 +159,61 @@ class DataExtensionTest extends SapphireTest {
$this->assertEquals("hello world", $mo->testMethodApplied());
$this->assertEquals("hello world", $do->testMethodApplied());
}
public function testPageFieldGeneration() {
$page = new DataExtensionTest_CMSFieldsBase();
$fields = $page->getCMSFields();
$this->assertNotEmpty($fields);
// Check basic field exists
$this->assertNotEmpty($fields->dataFieldByName('PageField'));
}
public function testPageExtensionsFieldGeneration() {
$page = new DataExtensionTest_CMSFieldsBase();
$fields = $page->getCMSFields();
$this->assertNotEmpty($fields);
// Check extending fields exist
$this->assertNotEmpty($fields->dataFieldByName('ExtendedFieldRemove')); // Not removed yet!
$this->assertNotEmpty($fields->dataFieldByName('ExtendedFieldKeep'));
}
public function testSubpageFieldGeneration() {
$page = new DataExtensionTest_CMSFieldsChild();
$fields = $page->getCMSFields();
$this->assertNotEmpty($fields);
// Check extending fields exist
$this->assertEmpty($fields->dataFieldByName('ExtendedFieldRemove')); // Removed by child class
$this->assertNotEmpty($fields->dataFieldByName('ExtendedFieldKeep'));
$this->assertNotEmpty($preExtendedField = $fields->dataFieldByName('ChildFieldBeforeExtension'));
$this->assertEquals($preExtendedField->Title(), 'ChildFieldBeforeExtension: Modified Title');
// Post-extension fields
$this->assertNotEmpty($fields->dataFieldByName('ChildField'));
}
public function testSubSubpageFieldGeneration() {
$page = new DataExtensionTest_CMSFieldsGrandchild();
$fields = $page->getCMSFields();
$this->assertNotEmpty($fields);
// Check extending fields exist
$this->assertEmpty($fields->dataFieldByName('ExtendedFieldRemove')); // Removed by child class
$this->assertNotEmpty($fields->dataFieldByName('ExtendedFieldKeep'));
// Check child fields removed by grandchild in beforeUpdateCMSFields
$this->assertEmpty($fields->dataFieldByName('ChildFieldBeforeExtension')); // Removed by grandchild class
// Check grandchild field modified by extension
$this->assertNotEmpty($preExtendedField = $fields->dataFieldByName('GrandchildFieldBeforeExtension'));
$this->assertEquals($preExtendedField->Title(), 'GrandchildFieldBeforeExtension: Modified Title');
// Post-extension fields
$this->assertNotEmpty($fields->dataFieldByName('ChildField'));
$this->assertNotEmpty($fields->dataFieldByName('GrandchildField'));
}
}
class DataExtensionTest_Member extends DataObject implements TestOnly {
@ -313,3 +371,93 @@ DataExtensionTest_MyObject::add_extension('DataExtensionTest_Ext1');
DataExtensionTest_MyObject::add_extension('DataExtensionTest_Ext2');
DataExtensionTest_MyObject::add_extension('DataExtensionTest_Faves');
/**
* Base class for CMS fields
*/
class DataExtensionTest_CMSFieldsBase extends DataObject implements TestOnly {
private static $db = array(
'PageField' => 'Varchar(255)'
);
private static $extensions = array(
'DataExtensionTest_CMSFieldsBaseExtension'
);
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.Test', new TextField('PageField'));
return $fields;
}
}
/**
* Extension to top level test class, tests that updateCMSFields work
*/
class DataExtensionTest_CMSFieldsBaseExtension extends DataExtension implements TestOnly {
private static $db = array(
'ExtendedFieldKeep' => 'Varchar(255)',
'ExtendedFieldRemove' => 'Varchar(255)'
);
public function updateCMSFields(FieldList $fields) {
$fields->addFieldToTab('Root.Test', new TextField('ExtendedFieldRemove'));
$fields->addFieldToTab('Root.Test', new TextField('ExtendedFieldKeep'));
if($childField = $fields->dataFieldByName('ChildFieldBeforeExtension')) {
$childField->setTitle('ChildFieldBeforeExtension: Modified Title');
}
if($grandchildField = $fields->dataFieldByName('GrandchildFieldBeforeExtension')) {
$grandchildField->setTitle('GrandchildFieldBeforeExtension: Modified Title');
}
}
}
/**
* Second level test class.
* Tests usage of beforeExtendingCMSFields
*/
class DataExtensionTest_CMSFieldsChild extends DataExtensionTest_CMSFieldsBase implements TestOnly {
private static $db = array(
'ChildField' => 'Varchar(255)',
'ChildFieldBeforeExtension' => 'Varchar(255)'
);
public function getCMSFields() {
$this->beforeExtending('updateCMSFields', function(FieldList $fields) {
$fields->addFieldToTab('Root.Test', new TextField('ChildFieldBeforeExtension'));
});
$this->afterExtending('updateCMSFields', function(FieldList $fields){
$fields->removeByName('ExtendedFieldRemove', true);
});
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.Test', new TextField('ChildField'));
return $fields;
}
}
/**
* Third level test class, testing that beforeExtendingCMSFields can be nested
*/
class DataExtensionTest_CMSFieldsGrandchild extends DataExtensionTest_CMSFieldsChild implements TestOnly {
private static $db = array(
'GrandchildField' => 'Varchar(255)'
);
public function getCMSFields() {
$this->beforeUpdateCMSFields(function(FieldList $fields) {
// Remove field from parent's beforeExtendingCMSFields
$fields->removeByName('ChildFieldBeforeExtension', true);
// Adds own pre-extension field
$fields->addFieldToTab('Root.Test', new TextField('GrandchildFieldBeforeExtension'));
});
$fields = parent::getCMSFields();
$fields->addFieldToTab('Root.Test', new TextField('GrandchildField'));
return $fields;
}
}