Complex classes like ModelAdmin often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ModelAdmin, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 15 | abstract class ModelAdmin extends LeftAndMain { |
||
| 16 | |||
| 17 | private static $url_rule = '/$ModelClass/$Action'; |
||
| 18 | |||
| 19 | /** |
||
| 20 | * List of all managed {@link DataObject}s in this interface. |
||
| 21 | * |
||
| 22 | * Simple notation with class names only: |
||
| 23 | * <code> |
||
| 24 | * array('MyObjectClass','MyOtherObjectClass') |
||
| 25 | * </code> |
||
| 26 | * |
||
| 27 | * Extended notation with options (e.g. custom titles): |
||
| 28 | * <code> |
||
| 29 | * array( |
||
| 30 | * 'MyObjectClass' => array('title' => "Custom title") |
||
| 31 | * ) |
||
| 32 | * </code> |
||
| 33 | * |
||
| 34 | * Available options: |
||
| 35 | * - 'title': Set custom titles for the tabs or dropdown names |
||
| 36 | * |
||
| 37 | * @config |
||
| 38 | * @var array|string |
||
| 39 | */ |
||
| 40 | private static $managed_models = null; |
||
| 41 | |||
| 42 | /** |
||
| 43 | * Override menu_priority so that ModelAdmin CMSMenu objects |
||
| 44 | * are grouped together directly above the Help menu item. |
||
| 45 | * @var float |
||
| 46 | */ |
||
| 47 | private static $menu_priority = -0.5; |
||
| 48 | |||
| 49 | private static $menu_icon = 'framework/admin/images/menu-icons/16x16/db.png'; |
||
| 50 | |||
| 51 | private static $allowed_actions = array( |
||
| 52 | 'ImportForm', |
||
| 53 | 'SearchForm', |
||
| 54 | ); |
||
| 55 | |||
| 56 | private static $url_handlers = array( |
||
| 57 | '$ModelClass/$Action' => 'handleAction' |
||
| 58 | ); |
||
| 59 | |||
| 60 | /** |
||
| 61 | * @var String |
||
| 62 | */ |
||
| 63 | protected $modelClass; |
||
| 64 | |||
| 65 | /** |
||
| 66 | * Change this variable if you don't want the Import from CSV form to appear. |
||
| 67 | * This variable can be a boolean or an array. |
||
| 68 | * If array, you can list className you want the form to appear on. i.e. array('myClassOne','myClasstwo') |
||
| 69 | */ |
||
| 70 | public $showImportForm = true; |
||
| 71 | |||
| 72 | /** |
||
| 73 | * List of all {@link DataObject}s which can be imported through |
||
| 74 | * a subclass of {@link BulkLoader} (mostly CSV data). |
||
| 75 | * By default {@link CsvBulkLoader} is used, assuming a standard mapping |
||
| 76 | * of column names to {@link DataObject} properties/relations. |
||
| 77 | * |
||
| 78 | * e.g. "BlogEntry" => "BlogEntryCsvBulkLoader" |
||
| 79 | * |
||
| 80 | * @config |
||
| 81 | * @var array |
||
| 82 | */ |
||
| 83 | private static $model_importers = null; |
||
| 84 | |||
| 85 | /** |
||
| 86 | * Amount of results showing on a single page. |
||
| 87 | * |
||
| 88 | * @config |
||
| 89 | * @var int |
||
| 90 | */ |
||
| 91 | private static $page_length = 30; |
||
| 92 | |||
| 93 | /** |
||
| 94 | * Initialize the model admin interface. Sets up embedded jquery libraries and requisite plugins. |
||
| 95 | */ |
||
| 96 | public function init() { |
||
| 115 | |||
| 116 | public function Link($action = null) { |
||
| 120 | |||
| 121 | public function getEditForm($id = null, $fields = null) { |
||
| 122 | $list = $this->getList(); |
||
| 123 | $exportButton = new GridFieldExportButton('buttons-before-left'); |
||
| 124 | $exportButton->setExportColumns($this->getExportFields()); |
||
| 125 | $listField = GridField::create( |
||
| 126 | $this->sanitiseClassName($this->modelClass), |
||
| 127 | false, |
||
| 128 | $list, |
||
| 129 | $fieldConfig = GridFieldConfig_RecordEditor::create($this->stat('page_length')) |
||
| 130 | ->addComponent($exportButton) |
||
| 131 | ->removeComponentsByType('GridFieldFilterHeader') |
||
| 132 | ->addComponents(new GridFieldPrintButton('buttons-before-left')) |
||
| 133 | ); |
||
| 134 | |||
| 135 | // Validation |
||
| 136 | if(singleton($this->modelClass)->hasMethod('getCMSValidator')) { |
||
| 137 | $detailValidator = singleton($this->modelClass)->getCMSValidator(); |
||
| 138 | $listField->getConfig()->getComponentByType('GridFieldDetailForm')->setValidator($detailValidator); |
||
|
|
|||
| 139 | } |
||
| 140 | |||
| 141 | $form = CMSForm::create( |
||
| 142 | $this, |
||
| 143 | 'EditForm', |
||
| 144 | new FieldList($listField), |
||
| 145 | new FieldList() |
||
| 146 | )->setHTMLID('Form_EditForm'); |
||
| 147 | $form->setResponseNegotiator($this->getResponseNegotiator()); |
||
| 148 | $form->addExtraClass('cms-edit-form cms-panel-padded center'); |
||
| 149 | $form->setTemplate($this->getTemplatesWithSuffix('_EditForm')); |
||
| 150 | $editFormAction = Controller::join_links($this->Link($this->sanitiseClassName($this->modelClass)), 'EditForm'); |
||
| 151 | $form->setFormAction($editFormAction); |
||
| 152 | $form->setAttribute('data-pjax-fragment', 'CurrentForm'); |
||
| 153 | |||
| 154 | $this->extend('updateEditForm', $form); |
||
| 155 | |||
| 156 | return $form; |
||
| 157 | } |
||
| 158 | |||
| 159 | /** |
||
| 160 | * Define which fields are used in the {@link getEditForm} GridField export. |
||
| 161 | * By default, it uses the summary fields from the model definition. |
||
| 162 | * |
||
| 163 | * @return array |
||
| 164 | */ |
||
| 165 | public function getExportFields() { |
||
| 166 | return singleton($this->modelClass)->summaryFields(); |
||
| 167 | } |
||
| 168 | |||
| 169 | /** |
||
| 170 | * @return SearchContext |
||
| 171 | */ |
||
| 172 | public function getSearchContext() { |
||
| 173 | $context = singleton($this->modelClass)->getDefaultSearchContext(); |
||
| 174 | |||
| 175 | // Namespace fields, for easier detection if a search is present |
||
| 176 | foreach($context->getFields() as $field) $field->setName(sprintf('q[%s]', $field->getName())); |
||
| 177 | foreach($context->getFilters() as $filter) $filter->setFullName(sprintf('q[%s]', $filter->getFullName())); |
||
| 178 | |||
| 179 | $this->extend('updateSearchContext', $context); |
||
| 180 | |||
| 181 | return $context; |
||
| 182 | } |
||
| 183 | |||
| 184 | /** |
||
| 185 | * @return Form |
||
| 186 | */ |
||
| 187 | public function SearchForm() { |
||
| 188 | $context = $this->getSearchContext(); |
||
| 189 | $form = new Form($this, "SearchForm", |
||
| 190 | $context->getSearchFields(), |
||
| 191 | new FieldList( |
||
| 192 | Object::create('FormAction', 'search', _t('MemberTableField.APPLY_FILTER', 'Apply Filter')) |
||
| 193 | ->setUseButtonTag(true)->addExtraClass('ss-ui-action-constructive'), |
||
| 194 | Object::create('ResetFormAction','clearsearch', _t('ModelAdmin.RESET','Reset')) |
||
| 195 | ->setUseButtonTag(true) |
||
| 196 | ), |
||
| 197 | new RequiredFields() |
||
| 198 | ); |
||
| 199 | $form->setFormMethod('get'); |
||
| 200 | $form->setFormAction($this->Link($this->sanitiseClassName($this->modelClass))); |
||
| 201 | $form->addExtraClass('cms-search-form'); |
||
| 202 | $form->disableSecurityToken(); |
||
| 203 | $form->loadDataFrom($this->getRequest()->getVars()); |
||
| 204 | |||
| 205 | $this->extend('updateSearchForm', $form); |
||
| 206 | |||
| 207 | return $form; |
||
| 208 | } |
||
| 209 | |||
| 210 | public function getList() { |
||
| 211 | $context = $this->getSearchContext(); |
||
| 212 | $params = $this->getRequest()->requestVar('q'); |
||
| 213 | |||
| 214 | if(is_array($params)) { |
||
| 215 | $params = ArrayLib::array_map_recursive('trim', $params); |
||
| 216 | |||
| 217 | // Parse all DateFields to handle user input non ISO 8601 dates |
||
| 218 | foreach($context->getFields() as $field) { |
||
| 219 | if($field instanceof DatetimeField && !empty($params[$field->getName()])) { |
||
| 220 | $params[$field->getName()] = date('Y-m-d', strtotime($params[$field->getName()])); |
||
| 221 | } |
||
| 222 | } |
||
| 223 | } |
||
| 224 | |||
| 225 | $list = $context->getResults($params); |
||
| 226 | |||
| 227 | $this->extend('updateList', $list); |
||
| 228 | |||
| 229 | return $list; |
||
| 230 | } |
||
| 231 | |||
| 232 | |||
| 233 | /** |
||
| 234 | * Returns managed models' create, search, and import forms |
||
| 235 | * @uses SearchContext |
||
| 236 | * @uses SearchFilter |
||
| 237 | * @return SS_List of forms |
||
| 238 | */ |
||
| 239 | protected function getManagedModelTabs() { |
||
| 240 | $models = $this->getManagedModels(); |
||
| 241 | $forms = new ArrayList(); |
||
| 242 | |||
| 243 | foreach($models as $class => $options) { |
||
| 244 | $forms->push(new ArrayData(array ( |
||
| 245 | 'Title' => $options['title'], |
||
| 246 | 'ClassName' => $class, |
||
| 247 | 'Link' => $this->Link($this->sanitiseClassName($class)), |
||
| 248 | 'LinkOrCurrent' => ($class == $this->modelClass) ? 'current' : 'link' |
||
| 249 | ))); |
||
| 250 | } |
||
| 251 | |||
| 252 | return $forms; |
||
| 253 | } |
||
| 254 | |||
| 255 | /** |
||
| 256 | * Sanitise a model class' name for inclusion in a link |
||
| 257 | * @return string |
||
| 258 | */ |
||
| 259 | protected function sanitiseClassName($class) { |
||
| 262 | |||
| 263 | /** |
||
| 264 | * Unsanitise a model class' name from a URL param |
||
| 265 | * @return string |
||
| 266 | */ |
||
| 267 | protected function unsanitiseClassName($class) { |
||
| 270 | |||
| 271 | /** |
||
| 272 | * @return array Map of class name to an array of 'title' (see {@link $managed_models}) |
||
| 273 | */ |
||
| 274 | public function getManagedModels() { |
||
| 298 | |||
| 299 | /** |
||
| 300 | * Returns all importers defined in {@link self::$model_importers}. |
||
| 301 | * If none are defined, we fall back to {@link self::managed_models} |
||
| 302 | * with a default {@link CsvBulkLoader} class. In this case the column names of the first row |
||
| 303 | * in the CSV file are assumed to have direct mappings to properties on the object. |
||
| 304 | * |
||
| 305 | * @return array Map of model class names to importer instances |
||
| 306 | */ |
||
| 307 | public function getModelImporters() { |
||
| 325 | |||
| 326 | /** |
||
| 327 | * Generate a CSV import form for a single {@link DataObject} subclass. |
||
| 328 | * |
||
| 329 | * @return Form |
||
| 330 | */ |
||
| 331 | public function ImportForm() { |
||
| 394 | |||
| 395 | /** |
||
| 396 | * Imports the submitted CSV file based on specifications given in |
||
| 397 | * {@link self::model_importers}. |
||
| 398 | * Redirects back with a success/failure message. |
||
| 399 | * |
||
| 400 | * @todo Figure out ajax submission of files via jQuery.form plugin |
||
| 401 | * |
||
| 402 | * @param array $data |
||
| 403 | * @param Form $form |
||
| 404 | * @param SS_HTTPRequest $request |
||
| 405 | */ |
||
| 406 | public function import($data, $form, $request) { |
||
| 451 | |||
| 452 | /** |
||
| 453 | * @return ArrayList |
||
| 454 | */ |
||
| 455 | public function Breadcrumbs($unlinked = false) { |
||
| 471 | |||
| 472 | /** |
||
| 473 | * overwrite the static page_length of the admin panel, |
||
| 474 | * should be called in the project _config file. |
||
| 475 | * |
||
| 476 | * @deprecated 4.0 Use "ModelAdmin.page_length" config setting |
||
| 477 | */ |
||
| 478 | public static function set_page_length($length){ |
||
| 482 | |||
| 483 | /** |
||
| 484 | * Return the static page_length of the admin, default as 30 |
||
| 485 | * |
||
| 486 | * @deprecated 4.0 Use "ModelAdmin.page_length" config setting |
||
| 487 | */ |
||
| 488 | public static function get_page_length(){ |
||
| 492 | |||
| 493 | } |
||
| 494 |
Let’s take a look at an example:
In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.
Available Fixes
Change the type-hint for the parameter:
Add an additional type-check:
Add the method to the interface: