diff --git a/src/Forms/Form.php b/src/Forms/Form.php index 7f9b2251f..db7044ac9 100644 --- a/src/Forms/Form.php +++ b/src/Forms/Form.php @@ -12,7 +12,6 @@ use SilverStripe\Control\RequestHandler; use SilverStripe\Control\Session; use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Injector\Injector; -use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\ValidationResult; @@ -136,7 +135,7 @@ class Form extends ViewableData implements HasRequestHandler /** * Populated by {@link loadDataFrom()}. * - * @var DataObject|null + * @var ViewableData|null */ protected $record; @@ -1223,10 +1222,10 @@ class Form extends ViewableData implements HasRequestHandler } /** - * Returns the DataObject that has given this form its data + * Returns the record that has given this form its data * through {@link loadDataFrom()}. * - * @return DataObject + * @return ViewableData */ public function getRecord() { @@ -1285,7 +1284,7 @@ class Form extends ViewableData implements HasRequestHandler const MERGE_AS_SUBMITTED_VALUE = 0b1000; /** - * Load data from the given DataObject or array. + * Load data from the given record or array. * * It will call $object->MyField to get the value of MyField. * If you passed an array, it will call $object[MyField]. @@ -1306,7 +1305,7 @@ class Form extends ViewableData implements HasRequestHandler * @uses FormField::setSubmittedValue() * @uses FormField::setValue() * - * @param array|DataObject $data + * @param array|ViewableData $data * @param int $mergeStrategy * For every field, {@link $data} is interrogated whether it contains a relevant property/key, and * what that property/key's value is. @@ -1351,7 +1350,7 @@ class Form extends ViewableData implements HasRequestHandler // If an object is passed, save it for historical reference through {@link getRecord()} // Also use this to determine if we are loading a submitted form, or loading - // from a dataobject + // from a record $submitted = true; if (is_object($data)) { $this->record = $data; @@ -1480,7 +1479,7 @@ class Form extends ViewableData implements HasRequestHandler * Save the contents of this form into the given data object. * It will make use of setCastedField() to do this. * - * @param DataObjectInterface $dataObject The object to save data into + * @param ViewableData&DataObjectInterface $dataObject The object to save data into * @param FieldList $fieldList An optional list of fields to process. This can be useful when you have a * form that has some fields that save to one object, and some that save to another. */ @@ -1523,7 +1522,7 @@ class Form extends ViewableData implements HasRequestHandler * {@link FieldList->dataFields()}, which filters out * any form-specific data like form-actions. * Calls {@link FormField->dataValue()} on each field, - * which returns a value suitable for insertion into a DataObject + * which returns a value suitable for insertion into a record * property. * * @return array diff --git a/src/Forms/FormField.php b/src/Forms/FormField.php index dc14e477b..e8c821f65 100644 --- a/src/Forms/FormField.php +++ b/src/Forms/FormField.php @@ -8,13 +8,13 @@ use SilverStripe\Control\Controller; use SilverStripe\Control\RequestHandler; use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Convert; -use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\ValidationResult; use SilverStripe\View\AttributesHTML; use SilverStripe\View\SSViewer; +use SilverStripe\View\ViewableData; /** * Represents a field in a form. @@ -454,11 +454,11 @@ class FormField extends RequestHandler } /** - * Method to save this form field into the given {@link DataObject}. + * Method to save this form field into the given record. * * By default, makes use of $this->dataValue() * - * @param DataObject|DataObjectInterface $record DataObject to save data into + * @param ViewableData|DataObjectInterface $record Record to save data into */ public function saveInto(DataObjectInterface $record) { @@ -697,7 +697,7 @@ class FormField extends RequestHandler * or a submitted form value they should override setSubmittedValue() instead. * * @param mixed $value Either the parent object, or array of source data being loaded - * @param array|DataObject $data {@see Form::loadDataFrom} + * @param array|ViewableData $data {@see Form::loadDataFrom} * @return $this */ public function setValue($value, $data = null) @@ -712,7 +712,7 @@ class FormField extends RequestHandler * data formats. * * @param mixed $value - * @param array|DataObject $data + * @param array|ViewableData $data * @return $this */ public function setSubmittedValue($value, $data = null) diff --git a/src/Forms/GridField/GridField.php b/src/Forms/GridField/GridField.php index 19210651c..2590aa758 100644 --- a/src/Forms/GridField/GridField.php +++ b/src/Forms/GridField/GridField.php @@ -20,11 +20,14 @@ use SilverStripe\Forms\GridField\FormAction\SessionStore; use SilverStripe\Forms\GridField\FormAction\StateStore; use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\DataList; -use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\Filterable; +use SilverStripe\ORM\Limitable; +use SilverStripe\ORM\Sortable; use SilverStripe\ORM\SS_List; use SilverStripe\View\HTML; +use SilverStripe\View\ViewableData; /** * Displays a {@link SS_List} in a grid format. @@ -83,12 +86,12 @@ class GridField extends FormField /** * Data source. * - * @var SS_List + * @var SS_List&Filterable&Sortable&Limitable */ protected $list = null; /** - * Class name of the DataObject that the GridField will display. + * Class name of the records that the GridField will display. * * Defaults to the value of $this->list->dataClass. * @@ -205,7 +208,7 @@ class GridField extends FormField } /** - * Returns a data class that is a DataObject type that this GridField should look like. + * Returns the class name of the record type that this GridField should contain. * * @return string * @@ -374,7 +377,7 @@ class GridField extends FormField /** * Set the data source. * - * @param SS_List $list + * @param SS_List&Filterable&Sortable&Limitable $list * * @return $this */ @@ -388,7 +391,7 @@ class GridField extends FormField /** * Get the data source. * - * @return SS_List + * @return SS_List&Filterable&Sortable&Limitable */ public function getList() { @@ -398,7 +401,7 @@ class GridField extends FormField /** * Get the data source after applying every {@link GridField_DataManipulator} to it. * - * @return SS_List + * @return SS_List&Filterable&Sortable&Limitable */ public function getManipulatedList() { @@ -461,7 +464,7 @@ class GridField extends FormField if (($request instanceof NullHTTPRequest) && Controller::has_curr()) { $request = Controller::curr()->getRequest(); } - + $stateStr = $this->getStateManager()->getStateFromRequest($this, $request); if ($stateStr) { $oldState = $this->getState(false); @@ -744,7 +747,7 @@ class GridField extends FormField /** * @param int $total * @param int $index - * @param DataObject $record + * @param ViewableData $record * @param array $attributes * @param string $content * @@ -762,7 +765,7 @@ class GridField extends FormField /** * @param int $total * @param int $index - * @param DataObject $record + * @param ViewableData $record * @param array $attributes * @param string $content * @@ -780,7 +783,7 @@ class GridField extends FormField /** * @param int $total * @param int $index - * @param DataObject $record + * @param ViewableData $record * * @return array */ @@ -798,7 +801,7 @@ class GridField extends FormField /** * @param int $total * @param int $index - * @param DataObject $record + * @param ViewableData $record * * @return array */ @@ -869,7 +872,7 @@ class GridField extends FormField /** * Get the value from a column. * - * @param DataObject $record + * @param ViewableData $record * @param string $column * * @return string @@ -922,7 +925,7 @@ class GridField extends FormField * Use of this method ensures that any special rules around the data for this gridfield are * followed. * - * @param DataObject $record + * @param ViewableData $record * @param string $fieldName * * @return mixed @@ -949,7 +952,7 @@ class GridField extends FormField /** * Get extra columns attributes used as HTML attributes. * - * @param DataObject $record + * @param ViewableData $record * @param string $column * * @return array diff --git a/src/Forms/GridField/GridFieldAddNewButton.php b/src/Forms/GridField/GridFieldAddNewButton.php index dffcabdcd..67d943657 100644 --- a/src/Forms/GridField/GridFieldAddNewButton.php +++ b/src/Forms/GridField/GridFieldAddNewButton.php @@ -2,7 +2,9 @@ namespace SilverStripe\Forms\GridField; +use LogicException; use SilverStripe\Control\Controller; +use SilverStripe\Core\ClassInfo; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\RelationList; use SilverStripe\View\ArrayData; @@ -12,8 +14,7 @@ use SilverStripe\View\SSViewer; * This component provides a button for opening the add new form provided by * {@link GridFieldDetailForm}. * - * Only returns a button if {@link DataObject->canCreate()} for this record - * returns true. + * Only returns a button if canCreate() for this record returns true. */ class GridFieldAddNewButton extends AbstractGridFieldComponent implements GridField_HTMLProvider { @@ -36,7 +37,16 @@ class GridFieldAddNewButton extends AbstractGridFieldComponent implements GridFi public function getHTMLFragments($gridField) { - $singleton = singleton($gridField->getModelClass()); + $modelClass = $gridField->getModelClass(); + $singleton = singleton($modelClass); + + if (!$singleton->hasMethod('canCreate')) { + throw new LogicException( + __CLASS__ . ' cannot be used with models that do not implement canCreate().' + . " Remove this component from your GridField or implement canCreate() on $modelClass" + ); + } + $context = []; if ($gridField->getList() instanceof RelationList) { $record = $gridField->getForm()->getRecord(); @@ -51,7 +61,7 @@ class GridFieldAddNewButton extends AbstractGridFieldComponent implements GridFi if (!$this->buttonName) { // provide a default button name, can be changed by calling {@link setButtonName()} on this component - $objectName = $singleton->i18n_singular_name(); + $objectName = $singleton->hasMethod('i18n_singular_name') ? $singleton->i18n_singular_name() : ClassInfo::shortName($singleton); $this->buttonName = _t('SilverStripe\\Forms\\GridField\\GridField.Add', 'Add {name}', ['name' => $objectName]); } diff --git a/src/Forms/GridField/GridFieldDataColumns.php b/src/Forms/GridField/GridFieldDataColumns.php index a852e3107..41b0713d9 100644 --- a/src/Forms/GridField/GridFieldDataColumns.php +++ b/src/Forms/GridField/GridFieldDataColumns.php @@ -4,7 +4,8 @@ namespace SilverStripe\Forms\GridField; use SilverStripe\Core\Convert; use InvalidArgumentException; -use SilverStripe\ORM\DataObject; +use LogicException; +use SilverStripe\View\ViewableData; /** * @see GridField @@ -87,7 +88,15 @@ class GridFieldDataColumns extends AbstractGridFieldComponent implements GridFie public function getDisplayFields($gridField) { if (!$this->displayFields) { - return singleton($gridField->getModelClass())->summaryFields(); + $modelClass = $gridField->getModelClass(); + $singleton = singleton($modelClass); + if (!$singleton->hasMethod('summaryFields')) { + throw new LogicException( + 'Cannot dynamically determine columns. Pass the column names to setDisplayFields()' + . " or implement a summaryFields() method on $modelClass" + ); + } + return $singleton->summaryFields(); } return $this->displayFields; } @@ -146,7 +155,7 @@ class GridFieldDataColumns extends AbstractGridFieldComponent implements GridFie * HTML for the column, content of the element. * * @param GridField $gridField - * @param DataObject $record Record displayed in this row + * @param ViewableData $record Record displayed in this row * @param string $columnName * @return string HTML for the column. Return NULL to skip. */ @@ -180,7 +189,7 @@ class GridFieldDataColumns extends AbstractGridFieldComponent implements GridFie * Attributes for the element containing the content returned by {@link getColumnContent()}. * * @param GridField $gridField - * @param DataObject $record displayed in this row + * @param ViewableData $record displayed in this row * @param string $columnName * @return array */ @@ -216,7 +225,7 @@ class GridFieldDataColumns extends AbstractGridFieldComponent implements GridFie /** * Translate a Object.RelationName.ColumnName $columnName into the value that ColumnName returns * - * @param DataObject $record + * @param ViewableData $record * @param string $columnName * @return string|null - returns null if it could not found a value */ @@ -269,7 +278,7 @@ class GridFieldDataColumns extends AbstractGridFieldComponent implements GridFie /** * * @param GridField $gridField - * @param DataObject $item + * @param ViewableData $item * @param string $fieldName * @param string $value * @return string diff --git a/src/Forms/GridField/GridFieldDeleteAction.php b/src/Forms/GridField/GridFieldDeleteAction.php index 35c3c1e69..a0d81c322 100644 --- a/src/Forms/GridField/GridFieldDeleteAction.php +++ b/src/Forms/GridField/GridFieldDeleteAction.php @@ -2,9 +2,12 @@ namespace SilverStripe\Forms\GridField; +use LogicException; use SilverStripe\Control\Controller; -use SilverStripe\ORM\DataObject; +use SilverStripe\ORM\DataList; +use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\ValidationException; +use SilverStripe\View\ViewableData; /** * This class is a {@link GridField} component that adds a delete action for @@ -72,13 +75,12 @@ class GridFieldDeleteAction extends AbstractGridFieldComponent implements GridFi /** * * @param GridField $gridField - * @param DataObject $record + * @param DataObjectInterface&ViewableData $record * @param string $columnName * @return string|null the attribles for the action */ public function getExtraData($gridField, $record, $columnName) { - $field = $this->getRemoveAction($gridField, $record, $columnName); if ($field) { @@ -105,7 +107,7 @@ class GridFieldDeleteAction extends AbstractGridFieldComponent implements GridFi * Return any special attributes that will be used for FormField::create_tag() * * @param GridField $gridField - * @param DataObject $record + * @param DataObjectInterface&ViewableData $record * @param string $columnName * @return array */ @@ -153,7 +155,7 @@ class GridFieldDeleteAction extends AbstractGridFieldComponent implements GridFi /** * * @param GridField $gridField - * @param DataObject $record + * @param DataObjectInterface&ViewableData $record * @param string $columnName * @return string|null the HTML for the column */ @@ -179,29 +181,39 @@ class GridFieldDeleteAction extends AbstractGridFieldComponent implements GridFi */ public function handleAction(GridField $gridField, $actionName, $arguments, $data) { + $list = $gridField->getList(); if ($actionName == 'deleterecord' || $actionName == 'unlinkrelation') { - /** @var DataObject $item */ - $item = $gridField->getList()->byID($arguments['RecordID']); + /** @var DataObjectInterface&ViewableData $item */ + $item = $list->byID($arguments['RecordID']); if (!$item) { return; } if ($actionName == 'deleterecord') { + $this->checkForRequiredMethod($item, 'canDelete'); + if (!$item->canDelete()) { throw new ValidationException( _t(__CLASS__ . '.DeletePermissionsFailure', "No delete permissions") ); } + if (!($list instanceof DataList)) { + // We need to make sure to exclude the item since the list items have already been determined. + // This must happen before deletion while the item still has its ID set. + $gridField->setList($list->exclude(['ID' => $item->ID])); + } $item->delete(); } else { + $this->checkForRequiredMethod($item, 'canEdit'); + if (!$item->canEdit()) { throw new ValidationException( _t(__CLASS__ . '.EditPermissionsFailure', "No permission to unlink record") ); } - $gridField->getList()->remove($item); + $list->remove($item); } } } @@ -209,16 +221,19 @@ class GridFieldDeleteAction extends AbstractGridFieldComponent implements GridFi /** * * @param GridField $gridField - * @param DataObject $record + * @param DataObjectInterface&ViewableData $record * @param string $columnName * @return GridField_FormAction|null */ private function getRemoveAction($gridField, $record, $columnName) { if ($this->getRemoveRelation()) { + $this->checkForRequiredMethod($record, 'canEdit'); + if (!$record->canEdit()) { return null; } + $title = _t(__CLASS__ . '.UnlinkRelation', "Unlink"); $field = GridField_FormAction::create( @@ -233,9 +248,12 @@ class GridFieldDeleteAction extends AbstractGridFieldComponent implements GridFi ->setDescription($title) ->setAttribute('aria-label', $title); } else { + $this->checkForRequiredMethod($record, 'canDelete'); + if (!$record->canDelete()) { return null; } + $title = _t(__CLASS__ . '.Delete', "Delete"); $field = GridField_FormAction::create( @@ -274,4 +292,20 @@ class GridFieldDeleteAction extends AbstractGridFieldComponent implements GridFi $this->removeRelation = (bool) $removeRelation; return $this; } + + /** + * Checks if a required method exists - and if not, throws an exception. + * + * @throws LogicException if the required method doesn't exist + */ + private function checkForRequiredMethod($record, string $method): void + { + if (!$record->hasMethod($method)) { + $modelClass = get_class($record); + throw new LogicException( + __CLASS__ . " cannot be used with models that don't implement {$method}()." + . " Remove this component from your GridField or implement {$method}() on $modelClass" + ); + } + } } diff --git a/src/Forms/GridField/GridFieldDetailForm.php b/src/Forms/GridField/GridFieldDetailForm.php index 0497dc564..07299cc93 100644 --- a/src/Forms/GridField/GridFieldDetailForm.php +++ b/src/Forms/GridField/GridFieldDetailForm.php @@ -3,6 +3,7 @@ namespace SilverStripe\Forms\GridField; use Closure; +use LogicException; use SilverStripe\Control\Director; use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPResponse; @@ -13,10 +14,11 @@ use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Extensible; use SilverStripe\Core\Injector\Injector; use SilverStripe\Forms\FieldList; +use SilverStripe\Forms\FieldsValidator; use SilverStripe\Forms\Validator; use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataObject; -use SilverStripe\ORM\Filterable; +use SilverStripe\View\ViewableData; /** * Provides view and edit forms at GridField-specific URLs. @@ -62,7 +64,7 @@ class GridFieldDetailForm extends AbstractGridFieldComponent implements GridFiel protected $validator; /** - * @var FieldList Falls back to {@link DataObject->getCMSFields()} if not defined. + * @var FieldList Falls back to {@link $record->getCMSFields()} if not defined. */ protected $fields; @@ -143,28 +145,31 @@ class GridFieldDetailForm extends AbstractGridFieldComponent implements GridFiel // if no validator has been set on the GridField then use the Validators from the record. if (!$this->getValidator()) { - $this->setValidator($record->getCMSCompositeValidator()); + if ($record->hasMethod('getCMSCompositeValidator')) { + $validator = $record->getCMSCompositeValidator(); + } else { + $validator = FieldsValidator::create(); + } + $this->setValidator($validator); } return $handler->handleRequest($request); } - /** - * @param GridField $gridField - * @param HTTPRequest $request - * @return DataObject|null - */ - protected function getRecordFromRequest(GridField $gridField, HTTPRequest $request): ?DataObject + protected function getRecordFromRequest(GridField $gridField, HTTPRequest $request): ?ViewableData { - /** @var DataObject $record */ + /** @var ViewableData $record */ if (is_numeric($request->param('ID'))) { - /** @var Filterable $dataList */ $dataList = $gridField->getList(); $record = $dataList->byID($request->param('ID')); } else { $record = Injector::inst()->create($gridField->getModelClass()); } + if ($record && !$record->hasField('ID')) { + throw new LogicException(get_class($record) . ' must have an ID field.'); + } + return $record; } @@ -174,7 +179,7 @@ class GridFieldDetailForm extends AbstractGridFieldComponent implements GridFiel * This only works when the list passed to the GridField is a {@link DataList}. * * @param $gridField The current GridField - * @param $id The ID of the DataObject to open + * @param $id The ID of the record to open */ public function getLostRecordRedirection(GridField $gridField, HTTPRequest $request, ?int $id = null): ?string { @@ -216,7 +221,7 @@ class GridFieldDetailForm extends AbstractGridFieldComponent implements GridFiel * Build a request handler for the given record * * @param GridField $gridField - * @param DataObject $record + * @param ViewableData $record * @param RequestHandler $requestHandler * @return GridFieldDetailForm_ItemRequest */ @@ -248,7 +253,7 @@ class GridFieldDetailForm extends AbstractGridFieldComponent implements GridFiel } /** - * @return String + * @return string */ public function getTemplate() { @@ -266,7 +271,7 @@ class GridFieldDetailForm extends AbstractGridFieldComponent implements GridFiel } /** - * @return String + * @return string */ public function getName() { @@ -276,8 +281,8 @@ class GridFieldDetailForm extends AbstractGridFieldComponent implements GridFiel /** * Enable redirection to missing records. * - * If a GridField shows a filtered list, and the DataObject is not in the list but exists in the - * database, and the DataObject has a CMSEditLink method, then the system will redirect to the + * If a GridField shows a filtered list, and the record is not in the list but exists in the + * database, and the record has a CMSEditLink method, then the system will redirect to the * URL returned by that method. */ public function setRedirectMissingRecords(bool $redirectMissingRecords): self diff --git a/src/Forms/GridField/GridFieldDetailForm_ItemRequest.php b/src/Forms/GridField/GridFieldDetailForm_ItemRequest.php index e3b5b8c3b..d0e6c6c54 100644 --- a/src/Forms/GridField/GridFieldDetailForm_ItemRequest.php +++ b/src/Forms/GridField/GridFieldDetailForm_ItemRequest.php @@ -9,6 +9,7 @@ use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\RequestHandler; use SilverStripe\Core\Convert; +use SilverStripe\Core\ClassInfo; use SilverStripe\Forms\CompositeField; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\Form; @@ -17,6 +18,7 @@ use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\LiteralField; use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\DataObject; +use SilverStripe\ORM\DataObjectInterface; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\HasManyList; use SilverStripe\ORM\ManyManyList; @@ -28,6 +30,7 @@ use SilverStripe\ORM\ValidationResult; use SilverStripe\View\ArrayData; use SilverStripe\View\HTML; use SilverStripe\View\SSViewer; +use SilverStripe\View\ViewableData; class GridFieldDetailForm_ItemRequest extends RequestHandler { @@ -64,7 +67,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler protected $component; /** - * @var DataObject + * @var ViewableData */ protected $record; @@ -96,7 +99,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler * * @param GridField $gridField * @param GridFieldDetailForm $component - * @param DataObject $record + * @param ViewableData&DataObjectInterface $record * @param RequestHandler $requestHandler * @param string $popupFormName */ @@ -125,11 +128,12 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler */ public function view($request) { - if (!$this->record->canView()) { + // Assume item can be viewed if canView() isn't implemented + if ($this->record->hasMethod('canView') && !$this->record->canView()) { $this->httpError(403, _t( __CLASS__ . '.ViewPermissionsFailure', 'It seems you don\'t have the necessary permissions to view "{ObjectTitle}"', - ['ObjectTitle' => $this->record->singular_name()] + ['ObjectTitle' => $this->getModelName()] )); } @@ -207,17 +211,25 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler } } - if (!$this->record->canView()) { + // Assume item can be viewed if canView() isn't implemented + if ($this->record->hasMethod('canView') && !$this->record->canView()) { $controller = $this->getToplevelController(); return $controller->httpError(403, _t( __CLASS__ . '.ViewPermissionsFailure', 'It seems you don\'t have the necessary permissions to view "{ObjectTitle}"', - ['ObjectTitle' => $this->record->singular_name()] + ['ObjectTitle' => $this->getModelName()] )); } $fields = $this->component->getFields(); if (!$fields) { + if (!$this->record->hasMethod('getCMSFields')) { + $modelClass = get_class($this->record); + throw new LogicException( + 'Cannot dynamically determine form fields. Pass the fields to GridFieldDetailForm::setFields()' + . " or implement a getCMSFields() method on {$modelClass}" + ); + } $fields = $this->record->getCMSFields(); } @@ -241,15 +253,15 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler $form->loadDataFrom($this->record, $this->record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT); - if ($this->record->ID && !$this->record->canEdit()) { + if ($this->record->ID && (!$this->record->hasMethod('canEdit') || !$this->record->canEdit())) { // Restrict editing of existing records $form->makeReadonly(); // Hack to re-enable delete button if user can delete - if ($this->record->canDelete()) { + if ($this->record->hasMethod('canDelete') && $this->record->canDelete()) { $form->Actions()->fieldByName('action_doDelete')->setReadonly(false); } } elseif (!$this->record->ID - && !$this->record->canCreate(null, $this->getCreateContext()) + && (!$this->record->hasMethod('canCreate') || !$this->record->canCreate(null, $this->getCreateContext())) ) { // Restrict creation of new records $form->makeReadonly(); @@ -359,7 +371,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler $rightGroup->push($previousAndNextGroup); - if ($component && $component->getShowAdd() && $this->record->canCreate()) { + if ($component && $component->getShowAdd() && $this->record->hasMethod('canCreate') && $this->record->canCreate()) { $rightGroup->push( LiteralField::create( 'new-record', @@ -378,7 +390,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler } /** - * Build the set of form field actions for this DataObject + * Build the set of form field actions for the record being handled * * @return FieldList */ @@ -391,8 +403,12 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler $majorActions->setFieldHolderTemplate(get_class($majorActions) . '_holder_buttongroup'); $actions->push($majorActions); - if ($this->record->ID !== 0) { // existing record - if ($this->record->canEdit()) { + if ($this->record->ID !== null && $this->record->ID !== 0) { // existing record + if ($this->record->hasMethod('canEdit') && $this->record->canEdit()) { + if (!($this->record instanceof DataObjectInterface)) { + throw new LogicException(get_class($this->record) . ' must implement ' . DataObjectInterface::class); + } + $noChangesClasses = 'btn-outline-primary font-icon-tick'; $majorActions->push(FormAction::create('doSave', _t('SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Save', 'Save')) ->addExtraClass($noChangesClasses) @@ -402,7 +418,10 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler ->setAttribute('data-text-alternate', _t('SilverStripe\\CMS\\Controllers\\CMSMain.SAVEDRAFT', 'Save'))); } - if ($this->record->canDelete()) { + if ($this->record->hasMethod('canDelete') && $this->record->canDelete()) { + if (!($this->record instanceof DataObjectInterface)) { + throw new LogicException(get_class($this->record) . ' must implement ' . DataObjectInterface::class); + } $actions->insertAfter('MajorActions', FormAction::create('doDelete', _t('SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Delete', 'Delete')) ->setUseButtonTag(true) ->addExtraClass('btn-outline-danger btn-hide-outline font-icon-trash-bin action--delete')); @@ -482,9 +501,9 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler * {@see Form::saveInto()} * * Handles detection of falsey values explicitly saved into the - * DataObject by formfields + * record by formfields * - * @param DataObject $record + * @param ViewableData $record * @param SS_List $list * @return array List of data to write to the relation */ @@ -510,11 +529,11 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler $isNewRecord = $this->record->ID == 0; // Check permission - if (!$this->record->canEdit()) { + if (!$this->record->hasMethod('canEdit') || !$this->record->canEdit()) { $this->httpError(403, _t( __CLASS__ . '.EditPermissionsFailure', 'It seems you don\'t have the necessary permissions to edit "{ObjectTitle}"', - ['ObjectTitle' => $this->record->singular_name()] + ['ObjectTitle' => $this->getModelName()] )); return null; } @@ -529,7 +548,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler 'SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Saved', 'Saved {name} {link}', [ - 'name' => $this->record->i18n_singular_name(), + 'name' => $this->getModelName(), 'link' => $link ] ); @@ -736,12 +755,12 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler } /** - * Loads the given form data into the underlying dataobject and relation + * Loads the given form data into the underlying record and relation * * @param array $data * @param Form $form * @throws ValidationException On error - * @return DataObject Saved record + * @return ViewableData&DataObjectInterface Saved record */ protected function saveFormIntoRecord($data, $form) { @@ -758,7 +777,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler $this->record = $this->record->newClassInstance($newClassName); } - // Save form and any extra saved data into this dataobject. + // Save form and any extra saved data into this record. // Set writeComponents = true to write has-one relations / join records $form->saveInto($this->record); // https://github.com/silverstripe/silverstripe-assets/issues/365 @@ -780,7 +799,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler public function doDelete($data, $form) { $title = $this->record->Title; - if (!$this->record->canDelete()) { + if (!$this->record->hasMethod('canDelete') || !$this->record->canDelete()) { throw new ValidationException( _t('SilverStripe\\Forms\\GridField\\GridFieldDetailForm.DeletePermissionsFailure', "No delete permissions") ); @@ -791,7 +810,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler 'SilverStripe\\Forms\\GridField\\GridFieldDetailForm.Deleted', 'Deleted {type} "{name}"', [ - 'type' => $this->record->i18n_singular_name(), + 'type' => $this->getModelName(), 'name' => htmlspecialchars($title ?? '', ENT_QUOTES) ] ); @@ -862,7 +881,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler } /** - * @return DataObject + * @return ViewableData */ public function getRecord() { @@ -898,7 +917,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler ])); } else { $items->push(ArrayData::create([ - 'Title' => _t('SilverStripe\\Forms\\GridField\\GridField.NewRecord', 'New {type}', ['type' => $this->record->i18n_singular_name()]), + 'Title' => _t('SilverStripe\\Forms\\GridField\\GridField.NewRecord', 'New {type}', ['type' => $this->getModelName()]), 'Link' => false ])); } @@ -912,4 +931,12 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler $this->extend('updateBreadcrumbs', $items); return $items; } + + private function getModelName(): string + { + if ($this->record->hasMethod('i18n_singular_name')) { + return $this->record->i18n_singular_name(); + } + return ClassInfo::shortName($this->record); + } } diff --git a/src/Forms/GridField/GridFieldEditButton.php b/src/Forms/GridField/GridFieldEditButton.php index 5c52c45d8..e3119fa62 100644 --- a/src/Forms/GridField/GridFieldEditButton.php +++ b/src/Forms/GridField/GridFieldEditButton.php @@ -3,9 +3,9 @@ namespace SilverStripe\Forms\GridField; use SilverStripe\Control\Controller; -use SilverStripe\ORM\DataObject; use SilverStripe\View\ArrayData; use SilverStripe\View\SSViewer; +use SilverStripe\View\ViewableData; /** * Provides the entry point to editing a single record presented by the @@ -91,7 +91,7 @@ class GridFieldEditButton extends AbstractGridFieldComponent implements GridFiel * Return any special attributes that will be used for FormField::create_tag() * * @param GridField $gridField - * @param DataObject $record + * @param ViewableData $record * @param string $columnName * @return array */ @@ -139,7 +139,7 @@ class GridFieldEditButton extends AbstractGridFieldComponent implements GridFiel /** * @param GridField $gridField - * @param DataObject $record + * @param ViewableData $record * @param string $columnName * @return string The HTML for the column */ diff --git a/src/Forms/GridField/GridFieldExportButton.php b/src/Forms/GridField/GridFieldExportButton.php index d52d9c821..7a1b63e48 100644 --- a/src/Forms/GridField/GridFieldExportButton.php +++ b/src/Forms/GridField/GridFieldExportButton.php @@ -3,12 +3,13 @@ namespace SilverStripe\Forms\GridField; use League\Csv\Writer; +use LogicException; use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPResponse; use SilverStripe\Core\Config\Config; use SilverStripe\ORM\DataList; -use SilverStripe\ORM\DataObject; use SilverStripe\ORM\ArrayList; +use SilverStripe\View\ViewableData; /** * Adds an "Export list" button to the bottom of a {@link GridField}. @@ -155,7 +156,15 @@ class GridFieldExportButton extends AbstractGridFieldComponent implements GridFi return $dataCols->getDisplayFields($gridField); } - return DataObject::singleton($gridField->getModelClass())->summaryFields(); + $modelClass = $gridField->getModelClass(); + $singleton = singleton($modelClass); + if (!$singleton->hasMethod('summaryFields')) { + throw new LogicException( + 'Cannot dynamically determine columns. Add a GridFieldDataColumns component to your GridField' + . " or implement a summaryFields() method on $modelClass" + ); + } + return $singleton->summaryFields(); } /** @@ -225,8 +234,9 @@ class GridFieldExportButton extends AbstractGridFieldComponent implements GridFi // Remove limit as the list may be paginated, we want the full list for the export $items = $items->limit(null); - /** @var DataObject $item */ + /** @var ViewableData $item */ foreach ($items as $item) { + // Assume item can be viewed if canView() isn't implemented if (!$item->hasMethod('canView') || $item->canView()) { $columnData = []; diff --git a/src/Forms/GridField/GridFieldFilterHeader.php b/src/Forms/GridField/GridFieldFilterHeader.php index 8af1a1fa1..edeb9f693 100755 --- a/src/Forms/GridField/GridFieldFilterHeader.php +++ b/src/Forms/GridField/GridFieldFilterHeader.php @@ -256,7 +256,15 @@ class GridFieldFilterHeader extends AbstractGridFieldComponent implements GridFi public function getSearchContext(GridField $gridField) { if (!$this->searchContext) { - $this->searchContext = singleton($gridField->getModelClass())->getDefaultSearchContext(); + $modelClass = $gridField->getModelClass(); + $singleton = singleton($modelClass); + if (!$singleton->hasMethod('getDefaultSearchContext')) { + throw new LogicException( + 'Cannot dynamically instantiate SearchContext. Pass the SearchContext to setSearchContext()' + . " or implement a getDefaultSearchContext() method on $modelClass" + ); + } + $this->searchContext = $singleton->getDefaultSearchContext(); } return $this->searchContext; diff --git a/src/Forms/GridField/GridFieldPrintButton.php b/src/Forms/GridField/GridFieldPrintButton.php index 1fc965808..0cf56bf6b 100644 --- a/src/Forms/GridField/GridFieldPrintButton.php +++ b/src/Forms/GridField/GridFieldPrintButton.php @@ -2,16 +2,17 @@ namespace SilverStripe\Forms\GridField; +use LogicException; use SilverStripe\Control\HTTPRequest; use SilverStripe\Core\Convert; use SilverStripe\Core\Extensible; use SilverStripe\ORM\ArrayList; -use SilverStripe\ORM\DataObject; use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\Security\Security; use SilverStripe\View\ArrayData; use SilverStripe\View\Requirements; +use SilverStripe\View\ViewableData; /** * Adds an "Print" button to the bottom or top of a GridField. @@ -161,7 +162,15 @@ class GridFieldPrintButton extends AbstractGridFieldComponent implements GridFie return $dataCols->getDisplayFields($gridField); } - return DataObject::singleton($gridField->getModelClass())->summaryFields(); + $modelClass = $gridField->getModelClass(); + $singleton = singleton($modelClass); + if (!$singleton->hasMethod('summaryFields')) { + throw new LogicException( + 'Cannot dynamically determine columns. Add a GridFieldDataColumns component to your GridField' + . " or implement a summaryFields() method on $modelClass" + ); + } + return $singleton->summaryFields(); } /** @@ -226,8 +235,9 @@ class GridFieldPrintButton extends AbstractGridFieldComponent implements GridFie /** @var GridFieldDataColumns $gridFieldColumnsComponent */ $gridFieldColumnsComponent = $gridField->getConfig()->getComponentByType(GridFieldDataColumns::class); - /** @var DataObject $item */ + /** @var ViewableData $item */ foreach ($items->limit(null) as $item) { + // Assume item can be viewed if canView() isn't implemented if (!$item->hasMethod('canView') || $item->canView()) { $itemRow = new ArrayList(); diff --git a/src/Forms/GridField/GridFieldSortableHeader.php b/src/Forms/GridField/GridFieldSortableHeader.php index 28cd0665f..1320bbc4c 100644 --- a/src/Forms/GridField/GridFieldSortableHeader.php +++ b/src/Forms/GridField/GridFieldSortableHeader.php @@ -11,6 +11,7 @@ use SilverStripe\ORM\DataObject; use SilverStripe\View\ArrayData; use SilverStripe\View\SSViewer; use LogicException; +use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\Deprecation; @@ -154,7 +155,7 @@ class GridFieldSortableHeader extends AbstractGridFieldComponent implements Grid if ($tmpItem instanceof SS_List) { // It's impossible to sort on a HasManyList/ManyManyList break; - } elseif ($tmpItem && method_exists($tmpItem, 'hasMethod') && $tmpItem->hasMethod($methodName)) { + } elseif ($tmpItem && ClassInfo::hasMethod($tmpItem, $methodName)) { // The part is a relation name, so get the object/list from it $tmpItem = $tmpItem->$methodName(); } elseif ($tmpItem instanceof DataObject diff --git a/src/Forms/GridField/GridFieldViewButton.php b/src/Forms/GridField/GridFieldViewButton.php index ea4129eb2..a0efccd6a 100644 --- a/src/Forms/GridField/GridFieldViewButton.php +++ b/src/Forms/GridField/GridFieldViewButton.php @@ -62,7 +62,8 @@ class GridFieldViewButton extends AbstractGridFieldComponent implements GridFiel public function getColumnContent($field, $record, $col) { - if (!$record->canView()) { + // Assume item can be viewed if canView() isn't implemented + if ($record->hasMethod('canView') && !$record->canView()) { return null; } $data = new ArrayData([ diff --git a/src/Forms/GridField/GridField_ActionMenuItem.php b/src/Forms/GridField/GridField_ActionMenuItem.php index 7f22cc15c..d07b23b88 100644 --- a/src/Forms/GridField/GridField_ActionMenuItem.php +++ b/src/Forms/GridField/GridField_ActionMenuItem.php @@ -2,7 +2,7 @@ namespace SilverStripe\Forms\GridField; -use SilverStripe\ORM\DataObject; +use SilverStripe\View\ViewableData; /** * GridField action menu item interface, this provides data so the action @@ -21,7 +21,7 @@ interface GridField_ActionMenuItem extends GridFieldComponent * @see {@link GridField_ActionMenu->getColumnContent()} * * @param GridField $gridField - * @param DataObject $record + * @param ViewableData $record * * @return string $title */ @@ -33,7 +33,7 @@ interface GridField_ActionMenuItem extends GridFieldComponent * @see {@link GridField_ActionMenu->getColumnContent()} * * @param GridField $gridField - * @param DataObject $record + * @param ViewableData $record * * @return array $data */ @@ -46,7 +46,7 @@ interface GridField_ActionMenuItem extends GridFieldComponent * @see {@link GridField_ActionMenu->getColumnContent()} * * @param GridField $gridField - * @param DataObject $record + * @param ViewableData $record * * @return string|null $group */ diff --git a/src/Forms/GridField/GridField_ActionMenuLink.php b/src/Forms/GridField/GridField_ActionMenuLink.php index d8727c840..77d0fa3f0 100644 --- a/src/Forms/GridField/GridField_ActionMenuLink.php +++ b/src/Forms/GridField/GridField_ActionMenuLink.php @@ -2,7 +2,7 @@ namespace SilverStripe\Forms\GridField; -use SilverStripe\ORM\DataObject; +use SilverStripe\View\ViewableData; /** * Allows GridField_ActionMenuItem to act as a link @@ -15,7 +15,7 @@ interface GridField_ActionMenuLink extends GridField_ActionMenuItem * @see {@link GridField_ActionMenu->getColumnContent()} * * @param GridField $gridField - * @param DataObject $record + * @param ViewableData $record * * @return string $url */ diff --git a/src/Forms/GridField/GridField_ColumnProvider.php b/src/Forms/GridField/GridField_ColumnProvider.php index 5863560f7..ee900ea40 100644 --- a/src/Forms/GridField/GridField_ColumnProvider.php +++ b/src/Forms/GridField/GridField_ColumnProvider.php @@ -34,7 +34,7 @@ interface GridField_ColumnProvider extends GridFieldComponent * HTML for the column, content of the element. * * @param GridField $gridField - * @param DataObject $record - Record displayed in this row + * @param ViewableData $record - Record displayed in this row * @param string $columnName * @return string - HTML for the column. Return NULL to skip. */ @@ -44,7 +44,7 @@ interface GridField_ColumnProvider extends GridFieldComponent * Attributes for the element containing the content returned by {@link getColumnContent()}. * * @param GridField $gridField - * @param DataObject $record displayed in this row + * @param ViewableData $record displayed in this row * @param string $columnName * @return array */ diff --git a/src/Forms/GridField/GridField_SaveHandler.php b/src/Forms/GridField/GridField_SaveHandler.php index ece1a5091..f56242615 100644 --- a/src/Forms/GridField/GridField_SaveHandler.php +++ b/src/Forms/GridField/GridField_SaveHandler.php @@ -3,6 +3,7 @@ namespace SilverStripe\Forms\GridField; use SilverStripe\ORM\DataObjectInterface; +use SilverStripe\View\ViewableData; /** * A component which is used to handle when a {@link GridField} is saved into @@ -15,7 +16,7 @@ interface GridField_SaveHandler extends GridFieldComponent * Called when a grid field is saved - i.e. the form is submitted. * * @param GridField $grid - * @param DataObjectInterface $record + * @param DataObjectInterface&ViewableData $record */ public function handleSave(GridField $grid, DataObjectInterface $record); } diff --git a/src/ORM/Search/SearchContext.php b/src/ORM/Search/SearchContext.php index d41e26042..54d44cd16 100644 --- a/src/ORM/Search/SearchContext.php +++ b/src/ORM/Search/SearchContext.php @@ -105,7 +105,7 @@ class SearchContext */ public function getSearchFields() { - if ($this->fields->exists()) { + if ($this->fields?->exists()) { return $this->fields; } @@ -443,7 +443,7 @@ class SearchContext */ public function addField($field) { - $this->fields->push($field); + $this->fields?->push($field); } /** @@ -453,7 +453,7 @@ class SearchContext */ public function removeFieldByName($fieldName) { - $this->fields->removeByName($fieldName); + $this->fields?->removeByName($fieldName); } /** @@ -500,7 +500,7 @@ class SearchContext continue; } - $field = $this->fields->fieldByName($filter->getFullName()); + $field = $this->fields?->fieldByName($filter->getFullName()); if (!$field) { continue; }