* $gridField = new GridField('ExampleGrid', 'Example grid', new DataList('Page')); * * * Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context, * since the required frontend dependencies are included through CMS bundling. * * @see SS_List * * @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::class); if (!$state) { $this->config->addComponent(new GridState_Component()); } $this->state = new GridState($this); $this->addExtraClass('grid-field'); } /** * @param 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; } /** @var DataList|ArrayList $list */ $list = $this->list; if ($list && $list->hasMethod('dataClass')) { $class = $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()) { $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 = HTML::createTag( 'td', array( 'colspan' => count($columns), ), _t('SilverStripe\\Forms\\GridField\\GridField.NoItemsFound', 'No items found') ); $row = HTML::createTag( '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 grid-field 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' => 'table grid-field__table', 'cellpadding' => '0', 'cellspacing' => '0' ); if ($this->getDescription()) { $content['after'] .= HTML::createTag( 'span', array('class' => 'description'), $this->getDescription() ); } $table = HTML::createTag( 'table', $tableAttributes, $header . "\n" . $footer . "\n" . $body ); return HTML::createTag( '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 HTML::createTag( '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 HTML::createTag( '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 HTTPRequest $request * * @return string */ public function gridFieldAlterAction($data, $form, HTTPRequest $request) { $data = $request->requestVars(); // Protection against CSRF attacks $token = $this ->getForm() ->getSecurityToken(); if (!$token->checkRequest($request)) { $this->httpError(400, _t( "SilverStripe\\Forms\\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); /** @skipUpgrade */ if (isset($fieldData['GridState'])) { $state->setValue($fieldData['GridState']); } foreach ($data as $dataKey => $dataValue) { if (preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) { $stateChange = $request->getSession()->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 HTTPRequest $request * @return array|RequestHandler|HTTPResponse|string * @throws HTTPResponse_Exception */ public function handleRequest(HTTPRequest $request) { if ($this->brokenOnConstruct) { user_error( sprintf( "parent::__construct() needs to be called on %s::__construct()", __CLASS__ ), E_USER_WARNING ); } $this->setRequest($request); $fieldData = $this->getRequest()->requestVar($this->getName()); /** @skipUpgrade */ 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 (HTTPResponse_Exception $responseException) { $result = $responseException->getResponse(); } if ($result instanceof HTTPResponse && $result->isError()) { return $result; } if ($this !== $result && !$request->isEmptyPattern($rule) && ($result instanceof RequestHandler || $result instanceof HasRequestHandler) ) { if ($result instanceof HasRequestHandler) { $result = $result->getRequestHandler(); } $returnValue = $result->handleRequest($request); 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); } /** * {@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 HTML::createTag( 'thead', array(), $content['header'] ); } return ''; } /** * @param array $content * * @return string */ protected function getOptionalTableBody(array $content) { if ($content['body']) { return HTML::createTag( 'tbody', array('class' => 'ss-gridfield-items'), $content['body'] ); } return ''; } /** * @param $content * * @return string */ protected function getOptionalTableFooter($content) { if ($content['footer']) { return HTML::createTag( 'tfoot', array(), $content['footer'] ); } return ''; } }