silverstripe-framework/forms/gridfield/GridField.php
Damian Mooyman 5c9044a007 API Enforce default_cast for all field usages
API Introduce HTMLFragment as casting helper for HTMLText with shortcodes disabled
API Introduce DBField::CDATA for XML file value encoding
API RSSFeed now casts from the underlying model rather than by override
API Introduce CustomMethods::getExtraMethodConfig() to allow metadata to be queried
BUG Remove _call hack from VirtualPage
API Remove FormField::$dontEscape
API Introduce HTMLReadonlyField for non-editable readonly HTML
API FormField::Field() now returns string in many cases rather than DBField instance.
API Remove redundant *_val methods from ViewableData
API ViewableData::obj() no longer has a $forceReturnObject parameter as it always returns an object
BUG  Fix issue with ViewableData caching incorrect field values after being modified.
API Remove deprecated DB class methods
API Enforce plain text left/right formfield titles
2016-07-13 17:15:45 +12:00

1198 lines
26 KiB
PHP

<?php
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\DataObjectInterface;
use SilverStripe\ORM\FieldType\DBHTMLText;
/**
* Displays a {@link SS_List} in a grid format.
*
* GridField is a field that takes an SS_List and displays it in an table with rows and columns.
* It reminds of the old TableFields but works with SS_List types and only loads the necessary
* rows from the list.
*
* The minimum configuration is to pass in name and title of the field and a SS_List.
*
* <code>
* $gridField = new GridField('ExampleGrid', 'Example grid', new DataList('Page'));
* </code>
*
* @see SS_List
*
* @package forms
* @subpackage fields-gridfield
* @property GridState_Data $State The gridstate of this object
*/
class GridField extends FormField {
/**
* @var array
*/
private static $allowed_actions = array(
'index',
'gridFieldAlterAction',
);
/**
* Data source.
*
* @var SS_List
*/
protected $list = null;
/**
* Class name of the DataObject that the GridField will display.
*
* Defaults to the value of $this->list->dataClass.
*
* @var string
*/
protected $modelClassName = '';
/**
* Current state of the GridField.
*
* @var GridState
*/
protected $state = null;
/**
* @var GridFieldConfig
*/
protected $config = null;
/**
* Components list.
*
* @var array
*/
protected $components = array();
/**
* Internal dispatcher for column handlers.
*
* Keys are column names and values are GridField_ColumnProvider objects.
*
* @var array
*/
protected $columnDispatch = null;
/**
* Map of callbacks for custom data fields.
*
* @var array
*/
protected $customDataFields = array();
/**
* @var string
*/
protected $name = '';
/**
* @param string $name
* @param string $title
* @param SS_List $dataList
* @param GridFieldConfig $config
*/
public function __construct($name, $title = null, SS_List $dataList = null, GridFieldConfig $config = null) {
parent::__construct($name, $title, null);
$this->name = $name;
if($dataList) {
$this->setList($dataList);
}
if(!$config) {
$config = GridFieldConfig_Base::create();
}
$this->setConfig($config);
$state = $this->config->getComponentByType('GridState_Component');
if (!$state) {
$this->config->addComponent(new GridState_Component());
}
$this->state = new GridState($this);
$this->addExtraClass('ss-gridfield');
}
/**
* @param SS_HTTPRequest $request
*
* @return string
*/
public function index($request) {
return $this->gridFieldAlterAction(array(), $this->getForm(), $request);
}
/**
* Set the modelClass (data object) that this field will get it column headers from.
*
* If no $displayFields has been set, the display fields will be $summary_fields.
*
* @see GridFieldDataColumns::getDisplayFields()
*
* @param string $modelClassName
*
* @return $this
*/
public function setModelClass($modelClassName) {
$this->modelClassName = $modelClassName;
return $this;
}
/**
* Returns a data class that is a DataObject type that this GridField should look like.
*
* @return string
*
* @throws LogicException
*/
public function getModelClass() {
if($this->modelClassName) {
return $this->modelClassName;
}
if($this->list && $this->list->hasMethod('dataClass')) {
$class = $this->list->dataClass();
if($class) {
return $class;
}
}
throw new LogicException(
'GridField doesn\'t have a modelClassName, so it doesn\'t know the columns of this grid.'
);
}
/**
* @return GridFieldConfig
*/
public function getConfig() {
return $this->config;
}
/**
* @param GridFieldConfig $config
*
* @return $this
*/
public function setConfig(GridFieldConfig $config) {
$this->config = $config;
return $this;
}
/**
* @return ArrayList
*/
public function getComponents() {
return $this->config->getComponents();
}
/**
* Cast an arbitrary value with the help of a $castingDefinition.
*
* @todo refactor this into GridFieldComponent
*
* @param mixed $value
* @param string|array $castingDefinition
*
* @return mixed
*/
public function getCastedValue($value, $castingDefinition) {
$castingParams = array();
if(is_array($castingDefinition)) {
$castingParams = $castingDefinition;
array_shift($castingParams);
$castingDefinition = array_shift($castingDefinition);
}
if(strpos($castingDefinition, '->') === false) {
$castingFieldType = $castingDefinition;
$castingField = DBField::create_field($castingFieldType, $value);
return call_user_func_array(array($castingField, 'XML'), $castingParams);
}
list($castingFieldType, $castingMethod) = explode('->', $castingDefinition);
$castingField = DBField::create_field($castingFieldType, $value);
return call_user_func_array(array($castingField, $castingMethod), $castingParams);
}
/**
* Set the data source.
*
* @param SS_List $list
*
* @return $this
*/
public function setList(SS_List $list) {
$this->list = $list;
return $this;
}
/**
* Get the data source.
*
* @return SS_List
*/
public function getList() {
return $this->list;
}
/**
* Get the data source after applying every {@link GridField_DataManipulator} to it.
*
* @return SS_List
*/
public function getManipulatedList() {
$list = $this->getList();
foreach($this->getComponents() as $item) {
if($item instanceof GridField_DataManipulator) {
$list = $item->getManipulatedData($this, $list);
}
}
return $list;
}
/**
* Get the current GridState_Data or the GridState.
*
* @param bool $getData
*
* @return GridState_Data|GridState
*/
public function getState($getData = true) {
if($getData) {
return $this->state->getData();
}
return $this->state;
}
/**
* Returns the whole gridfield rendered with all the attached components.
*
* @param array $properties
* @return string
*/
public function FieldHolder($properties = array()) {
Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css');
Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/GridField.css');
Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js');
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery-ui/jquery-ui.js');
Requirements::javascript(FRAMEWORK_DIR . '/client/dist/js/i18n.js');
Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/client/lang');
Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js');
Requirements::javascript(FRAMEWORK_DIR . '/client/dist/js/GridField.js');
$columns = $this->getColumns();
$list = $this->getManipulatedList();
$content = array(
'before' => '',
'after' => '',
'header' => '',
'footer' => '',
);
foreach($this->getComponents() as $item) {
if($item instanceof GridField_HTMLProvider) {
$fragments = $item->getHTMLFragments($this);
if($fragments) {
foreach($fragments as $fragmentKey => $fragmentValue) {
$fragmentKey = strtolower($fragmentKey);
if(!isset($content[$fragmentKey])) {
$content[$fragmentKey] = '';
}
$content[$fragmentKey] .= $fragmentValue . "\n";
}
}
}
}
foreach($content as $contentKey => $contentValue) {
$content[$contentKey] = trim($contentValue);
}
// Replace custom fragments and check which fragments are defined. Circular dependencies
// are detected by disallowing any item to be deferred more than 5 times.
$fragmentDefined = array(
'header' => true,
'footer' => true,
'before' => true,
'after' => true,
);
reset($content);
while(list($contentKey, $contentValue) = each($content)) {
if(preg_match_all('/\$DefineFragment\(([a-z0-9\-_]+)\)/i', $contentValue, $matches)) {
foreach($matches[1] as $match) {
$fragmentName = strtolower($match);
$fragmentDefined[$fragmentName] = true;
$fragment = '';
if(isset($content[$fragmentName])) {
$fragment = $content[$fragmentName];
}
// If the fragment still has a fragment definition in it, when we should defer
// this item until later.
if(preg_match('/\$DefineFragment\(([a-z0-9\-_]+)\)/i', $fragment, $matches)) {
if(isset($fragmentDeferred[$contentKey]) && $fragmentDeferred[$contentKey] > 5) {
throw new LogicException(sprintf(
'GridField HTML fragment "%s" and "%s" appear to have a circular dependency.',
$fragmentName,
$matches[1]
));
}
unset($content[$contentKey]);
$content[$contentKey] = $contentValue;
if(!isset($fragmentDeferred[$contentKey])) {
$fragmentDeferred[$contentKey] = 0;
}
$fragmentDeferred[$contentKey]++;
break;
} else {
$content[$contentKey] = preg_replace(
sprintf('/\$DefineFragment\(%s\)/i', $fragmentName),
$fragment,
$content[$contentKey]
);
}
}
}
}
// Check for any undefined fragments, and if so throw an exception.
// While we're at it, trim whitespace off the elements.
foreach($content as $contentKey => $contentValue) {
if(empty($fragmentDefined[$contentKey])) {
throw new LogicException(sprintf(
'GridField HTML fragment "%s" was given content, but not defined. Perhaps there is a supporting GridField component you need to add?',
$contentKey
));
}
}
$total = count($list);
if($total > 0) {
$rows = array();
foreach($list as $index => $record) {
if($record->hasMethod('canView') && !$record->canView()) {
continue;
}
$rowContent = '';
foreach($this->getColumns() as $column) {
$colContent = $this->getColumnContent($record, $column);
// Null means this columns should be skipped altogether.
if($colContent === null) {
continue;
}
$colAttributes = $this->getColumnAttributes($record, $column);
$rowContent .= $this->newCell(
$total,
$index,
$record,
$colAttributes,
$colContent
);
}
$rowAttributes = $this->getRowAttributes($total, $index, $record);
$rows[] = $this->newRow($total, $index, $record, $rowAttributes, $rowContent);
}
$content['body'] = implode("\n", $rows);
}
// Display a message when the grid field is empty.
if(empty($content['body'])) {
$cell = FormField::create_tag(
'td',
array(
'colspan' => count($columns),
),
_t('GridField.NoItemsFound', 'No items found')
);
$row = FormField::create_tag(
'tr',
array(
'class' => 'ss-gridfield-item ss-gridfield-no-items',
),
$cell
);
$content['body'] = $row;
}
$header = $this->getOptionalTableHeader($content);
$body = $this->getOptionalTableBody($content);
$footer = $this->getOptionalTableFooter($content);
$this->addExtraClass('ss-gridfield field');
$fieldsetAttributes = array_diff_key(
$this->getAttributes(),
array(
'value' => false,
'type' => false,
'name' => false,
)
);
$fieldsetAttributes['data-name'] = $this->getName();
$tableId = null;
if($this->id) {
$tableId = $this->id;
}
$tableAttributes = array(
'id' => $tableId,
'class' => 'ss-gridfield-table',
'cellpadding' => '0',
'cellspacing' => '0',
);
if($this->getDescription()) {
$content['after'] .= FormField::create_tag(
'span',
array('class' => 'description'),
$this->getDescription()
);
}
$table = FormField::create_tag(
'table',
$tableAttributes,
$header . "\n" . $footer . "\n" . $body
);
return FormField::create_tag(
'fieldset',
$fieldsetAttributes,
$content['before'] . $table . $content['after']
);
}
/**
* @param int $total
* @param int $index
* @param DataObject $record
* @param array $attributes
* @param string $content
*
* @return string
*/
protected function newCell($total, $index, $record, $attributes, $content) {
return FormField::create_tag(
'td',
$attributes,
$content
);
}
/**
* @param int $total
* @param int $index
* @param DataObject $record
* @param array $attributes
* @param string $content
*
* @return string
*/
protected function newRow($total, $index, $record, $attributes, $content) {
return FormField::create_tag(
'tr',
$attributes,
$content
);
}
/**
* @param int $total
* @param int $index
* @param DataObject $record
*
* @return array
*/
protected function getRowAttributes($total, $index, $record) {
$rowClasses = $this->newRowClasses($total, $index, $record);
return array(
'class' => implode(' ', $rowClasses),
'data-id' => $record->ID,
'data-class' => $record->ClassName,
);
}
/**
* @param int $total
* @param int $index
* @param DataObject $record
*
* @return array
*/
protected function newRowClasses($total, $index, $record) {
$classes = array('ss-gridfield-item');
if($index == 0) {
$classes[] = 'first';
}
if($index == $total - 1) {
$classes[] = 'last';
}
if($index % 2) {
$classes[] = 'even';
} else {
$classes[] = 'odd';
}
$this->extend('updateNewRowClasses', $classes, $total, $index, $record);
return $classes;
}
/**
* @param array $properties
* @return string
*/
public function Field($properties = array()) {
$this->extend('onBeforeRender', $this);
return $this->FieldHolder($properties);
}
/**
* {@inheritdoc}
*/
public function getAttributes() {
return array_merge(
parent::getAttributes(),
array(
'data-url' => $this->Link(),
)
);
}
/**
* Get the columns of this GridField, they are provided by attached GridField_ColumnProvider.
*
* @return array
*/
public function getColumns() {
$columns = array();
foreach($this->getComponents() as $item) {
if($item instanceof GridField_ColumnProvider) {
$item->augmentColumns($this, $columns);
}
}
return $columns;
}
/**
* Get the value from a column.
*
* @param DataObject $record
* @param string $column
*
* @return string
*
* @throws InvalidArgumentException
*/
public function getColumnContent($record, $column) {
if(!$this->columnDispatch) {
$this->buildColumnDispatch();
}
if(!empty($this->columnDispatch[$column])) {
$content = '';
foreach($this->columnDispatch[$column] as $handler) {
/**
* @var GridField_ColumnProvider $handler
*/
$content .= $handler->getColumnContent($this, $record, $column);
}
return $content;
} else {
throw new InvalidArgumentException(sprintf(
'Bad column "%s"',
$column
));
}
}
/**
* Add additional calculated data fields to be used on this GridField
*
* @param array $fields a map of fieldname to callback. The callback will
* be passed the record as an argument.
*/
public function addDataFields($fields) {
if($this->customDataFields) {
$this->customDataFields = array_merge($this->customDataFields, $fields);
} else {
$this->customDataFields = $fields;
}
}
/**
* Get the value of a named field on the given record.
*
* Use of this method ensures that any special rules around the data for this gridfield are
* followed.
*
* @param DataObject $record
* @param string $fieldName
*
* @return mixed
*/
public function getDataFieldValue($record, $fieldName) {
if(isset($this->customDataFields[$fieldName])) {
$callback = $this->customDataFields[$fieldName];
return $callback($record);
}
if($record->hasMethod('relField')) {
return $record->relField($fieldName);
}
if($record->hasMethod($fieldName)) {
return $record->$fieldName();
}
return $record->$fieldName;
}
/**
* Get extra columns attributes used as HTML attributes.
*
* @param DataObject $record
* @param string $column
*
* @return array
*
* @throws LogicException
* @throws InvalidArgumentException
*/
public function getColumnAttributes($record, $column) {
if(!$this->columnDispatch) {
$this->buildColumnDispatch();
}
if(!empty($this->columnDispatch[$column])) {
$attributes = array();
foreach($this->columnDispatch[$column] as $handler) {
/**
* @var GridField_ColumnProvider $handler
*/
$columnAttributes = $handler->getColumnAttributes($this, $record, $column);
if(is_array($columnAttributes)) {
$attributes = array_merge($attributes, $columnAttributes);
continue;
}
throw new LogicException(sprintf(
'Non-array response from %s::getColumnAttributes().',
get_class($handler)
));
}
return $attributes;
}
throw new InvalidArgumentException(sprintf(
'Bad column "%s"',
$column
));
}
/**
* Get metadata for a column.
*
* @example "array('Title'=>'Email address')"
*
* @param string $column
*
* @return array
*
* @throws LogicException
* @throws InvalidArgumentException
*/
public function getColumnMetadata($column) {
if(!$this->columnDispatch) {
$this->buildColumnDispatch();
}
if(!empty($this->columnDispatch[$column])) {
$metaData = array();
foreach($this->columnDispatch[$column] as $handler) {
/**
* @var GridField_ColumnProvider $handler
*/
$columnMetaData = $handler->getColumnMetadata($this, $column);
if(is_array($columnMetaData)) {
$metaData = array_merge($metaData, $columnMetaData);
continue;
}
throw new LogicException(sprintf(
'Non-array response from %s::getColumnMetadata().',
get_class($handler)
));
}
return $metaData;
}
throw new InvalidArgumentException(sprintf(
'Bad column "%s"',
$column
));
}
/**
* Return how many columns the grid will have.
*
* @return int
*/
public function getColumnCount() {
if(!$this->columnDispatch) {
$this->buildColumnDispatch();
}
return count($this->columnDispatch);
}
/**
* Build an columnDispatch that maps a GridField_ColumnProvider to a column for reference later.
*/
protected function buildColumnDispatch() {
$this->columnDispatch = array();
foreach($this->getComponents() as $item) {
if($item instanceof GridField_ColumnProvider) {
$columns = $item->getColumnsHandled($this);
foreach($columns as $column) {
$this->columnDispatch[$column][] = $item;
}
}
}
}
/**
* This is the action that gets executed when a GridField_AlterAction gets clicked.
*
* @param array $data
* @param Form $form
* @param SS_HTTPRequest $request
*
* @return string
*/
public function gridFieldAlterAction($data, $form, SS_HTTPRequest $request) {
$data = $request->requestVars();
// Protection against CSRF attacks
$token = $this
->getForm()
->getSecurityToken();
if(!$token->checkRequest($request)) {
$this->httpError(400, _t("Form.CSRF_FAILED_MESSAGE",
"There seems to have been a technical problem. Please click the back button, ".
"refresh your browser, and try again."
));
}
$name = $this->getName();
$fieldData = null;
if(isset($data[$name])) {
$fieldData = $data[$name];
}
$state = $this->getState(false);
if(isset($fieldData['GridState'])) {
$state->setValue($fieldData['GridState']);
}
foreach($data as $dataKey => $dataValue) {
if(preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) {
$stateChange = Session::get($matches[1]);
$actionName = $stateChange['actionName'];
$arguments = array();
if(isset($stateChange['args'])) {
$arguments = $stateChange['args'];
};
$html = $this->handleAlterAction($actionName, $arguments, $data);
if($html) {
return $html;
}
}
}
if($request->getHeader('X-Pjax') === 'CurrentField') {
return $this->FieldHolder();
}
return $form->forTemplate();
}
/**
* Pass an action on the first GridField_ActionProvider that matches the $actionName.
*
* @param string $actionName
* @param mixed $arguments
* @param array $data
*
* @return mixed
*
* @throws InvalidArgumentException
*/
public function handleAlterAction($actionName, $arguments, $data) {
$actionName = strtolower($actionName);
foreach($this->getComponents() as $component) {
if($component instanceof GridField_ActionProvider) {
$actions = array_map('strtolower', (array) $component->getActions($this));
if(in_array($actionName, $actions)) {
return $component->handleAction($this, $actionName, $arguments, $data);
}
}
}
throw new InvalidArgumentException(sprintf(
'Can\'t handle action "%s"',
$actionName
));
}
/**
* Custom request handler that will check component handlers before proceeding to the default
* implementation.
*
* @todo copy less code from RequestHandler.
*
* @param SS_HTTPRequest $request
* @param DataModel $model
*
* @return array|RequestHandler|SS_HTTPResponse|string|void
*
* @throws SS_HTTPResponse_Exception
*/
public function handleRequest(SS_HTTPRequest $request, DataModel $model) {
if($this->brokenOnConstruct) {
user_error(
sprintf(
"parent::__construct() needs to be called on %s::__construct()",
__CLASS__
),
E_USER_WARNING
);
}
$this->setRequest($request);
$this->setDataModel($model);
$fieldData = $this->getRequest()->requestVar($this->getName());
if($fieldData && isset($fieldData['GridState'])) {
$this->getState(false)->setValue($fieldData['GridState']);
}
foreach($this->getComponents() as $component) {
if($component instanceof GridField_URLHandler && $urlHandlers = $component->getURLHandlers($this)) {
foreach($urlHandlers as $rule => $action) {
if($params = $request->match($rule, true)) {
// Actions can reference URL parameters.
// e.g. '$Action/$ID/$OtherID' → '$Action'
if($action[0] == '$') {
$action = $params[substr($action, 1)];
}
if(!method_exists($component, 'checkAccessAction') || $component->checkAccessAction($action)) {
if(!$action) {
$action = "index";
}
if(!is_string($action)) {
throw new LogicException(sprintf(
'Non-string method name: %s',
var_export($action, true)
));
}
try {
$result = $component->$action($this, $request);
} catch(SS_HTTPResponse_Exception $responseException) {
$result = $responseException->getResponse();
}
if($result instanceof SS_HTTPResponse && $result->isError()) {
return $result;
}
if($this !== $result && !$request->isEmptyPattern($rule) && is_object($result) && $result instanceof RequestHandler) {
$returnValue = $result->handleRequest($request, $model);
if(is_array($returnValue)) {
throw new LogicException(
'GridField_URLHandler handlers can\'t return arrays'
);
}
return $returnValue;
}
if($request->allParsed()) {
return $result;
}
return $this->httpError(
404,
sprintf(
'I can\'t handle sub-URLs of a %s object.',
get_class($result)
)
);
}
}
}
}
}
return parent::handleRequest($request, $model);
}
/**
* {@inheritdoc}
*/
public function saveInto(DataObjectInterface $record) {
foreach($this->getComponents() as $component) {
if($component instanceof GridField_SaveHandler) {
$component->handleSave($this, $record);
}
}
}
/**
* @param array $content
*
* @return string
*/
protected function getOptionalTableHeader(array $content) {
if($content['header']) {
return FormField::create_tag(
'thead', array(), $content['header']
);
}
return '';
}
/**
* @param array $content
*
* @return string
*/
protected function getOptionalTableBody(array $content) {
if($content['body']) {
return FormField::create_tag(
'tbody', array('class' => 'ss-gridfield-items'), $content['body']
);
}
return '';
}
/**
* @param $content
*
* @return string
*/
protected function getOptionalTableFooter($content) {
if($content['footer']) {
return FormField::create_tag(
'tfoot', array(), $content['footer']
);
}
return '';
}
}
/**
* This class is the base class when you want to have an action that alters the state of the
* {@link GridField}, rendered as a button element.
*
* @package forms
* @subpackage fields-gridfield
*/
class GridField_FormAction extends FormAction {
/**
* @var GridField
*/
protected $gridField;
/**
* @var array
*/
protected $stateValues;
/**
* @var array
*/
protected $args = array();
/**
* @var string
*/
protected $actionName;
/**
* @var boolean
*/
public $useButtonTag = true;
/**
* @param GridField $gridField
* @param string $name
* @param string $title
* @param string $actionName
* @param array $args
*/
public function __construct(GridField $gridField, $name, $title, $actionName, $args) {
$this->gridField = $gridField;
$this->actionName = $actionName;
$this->args = $args;
parent::__construct($name, $title);
}
/**
* Encode all non-word characters.
*
* @param string $value
*
* @return string
*/
public function nameEncode($value) {
return (string) preg_replace_callback('/[^\w]/', array($this, '_nameEncode'), $value);
}
/**
* @param array $match
*
* @return string
*/
public function _nameEncode($match) {
return '%' . dechex(ord($match[0]));
}
/**
* @return array
*/
public function getAttributes() {
// Store state in session, and pass ID to client side.
$state = array(
'grid' => $this->getNameFromParent(),
'actionName' => $this->actionName,
'args' => $this->args,
);
// Ensure $id doesn't contain only numeric characters
$id = 'gf_' . substr(md5(serialize($state)), 0, 8);
Session::set($id, $state);
$actionData['StateID'] = $id;
return array_merge(
parent::getAttributes(),
array(
// Note: This field needs to be less than 65 chars, otherwise Suhosin security patch
// will strip it from the requests
'name' => 'action_gridFieldAlterAction' . '?' . http_build_query($actionData),
'data-url' => $this->gridField->Link(),
)
);
}
/**
* Calculate the name of the gridfield relative to the form.
*
* @return string
*/
protected function getNameFromParent() {
$base = $this->gridField;
$name = array();
do {
array_unshift($name, $base->getName());
$base = $base->getForm();
} while($base && !($base instanceof Form));
return implode('.', $name);
}
}