mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
NEW Added beforeExtending, afterExtending, and beforeUpdateCMSFields to allow user code better control over interaction with extending methods
This commit is contained in:
parent
9f532fe976
commit
6e0e3564e1
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user