Permalink
Browse files

NEW Added beforeExtending, afterExtending, and beforeUpdateCMSFields …

…to allow user code better control over interaction with extending methods
  • Loading branch information...
1 parent 9f532fe commit 6e0e3564e1830444f8762b366c3f3259733d865d @tractorcow tractorcow committed May 15, 2013
View
@@ -67,6 +67,52 @@ static public function config() {
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
*
* This method first for strong class overloads (singletons & DB interaction), then custom class overloads. If an
@@ -929,6 +975,14 @@ public function invokeWithExtensions($method, $argument = null) {
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 @@ public function extend($method, &$a1=null, &$a2=null, &$a3=null, &$a4=null, &$a5
}
}
+ 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
View
@@ -2000,6 +2000,16 @@ public function scaffoldFormFields($_params = null) {
}
/**
+ * 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.
* If not overloaded, we're using {@link scaffoldFormFields()} to automatically
@@ -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 @@ public function testExtensionCanBeAppliedToDataObject() {
$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 @@ public function testMethodApplied() {
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;
+ }
+}

0 comments on commit 6e0e356

Please sign in to comment.