Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like AssetAdmin 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 AssetAdmin, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
12 | class AssetAdmin extends LeftAndMain implements PermissionProvider{ |
||
13 | |||
14 | private static $url_segment = 'assets'; |
||
15 | |||
16 | private static $url_rule = '/$Action/$ID'; |
||
17 | |||
18 | private static $menu_title = 'Files'; |
||
19 | |||
20 | private static $tree_class = 'Folder'; |
||
21 | |||
22 | /** |
||
23 | * Amount of results showing on a single page. |
||
24 | * |
||
25 | * @config |
||
26 | * @var int |
||
27 | */ |
||
28 | private static $page_length = 15; |
||
29 | |||
30 | /** |
||
31 | * @config |
||
32 | * @see Upload->allowedMaxFileSize |
||
33 | * @var int |
||
34 | */ |
||
35 | private static $allowed_max_file_size; |
||
36 | |||
37 | private static $allowed_actions = array( |
||
38 | 'addfolder', |
||
39 | 'delete', |
||
40 | 'AddForm', |
||
41 | 'SearchForm', |
||
42 | 'getsubtree' |
||
43 | ); |
||
44 | |||
45 | /** |
||
46 | * Return fake-ID "root" if no ID is found (needed to upload files into the root-folder) |
||
47 | */ |
||
48 | public function currentPageID() { |
||
59 | |||
60 | /** |
||
61 | * Set up the controller |
||
62 | */ |
||
63 | public function init() { |
||
64 | parent::init(); |
||
65 | |||
66 | Versioned::set_stage(Versioned::DRAFT); |
||
67 | |||
68 | Requirements::javascript(CMS_DIR . "/client/dist/js/AssetAdmin.js"); |
||
69 | Requirements::add_i18n_javascript(CMS_DIR . '/client/src/lang', false, true); |
||
70 | Requirements::css(CMS_DIR . '/client/dist/styles/bundle.css'); |
||
71 | CMSBatchActionHandler::register('delete', 'AssetAdmin_DeleteBatchAction', 'Folder'); |
||
72 | } |
||
73 | |||
74 | /** |
||
75 | * Returns the files and subfolders contained in the currently selected folder, |
||
76 | * defaulting to the root node. Doubles as search results, if any search parameters |
||
77 | * are set through {@link SearchForm()}. |
||
78 | * |
||
79 | * @return SS_List |
||
80 | */ |
||
81 | public function getList() { |
||
137 | |||
138 | public function getEditForm($id = null, $fields = null) { |
||
139 | Requirements::javascript(FRAMEWORK_DIR . '/client/dist/js/AssetUploadField.js'); |
||
140 | Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/AssetUploadField.css'); |
||
141 | |||
142 | $form = parent::getEditForm($id, $fields); |
||
143 | $folder = ($id && is_numeric($id)) ? DataObject::get_by_id('Folder', $id, false) : $this->currentPage(); |
||
144 | $fields = $form->Fields(); |
||
145 | $title = ($folder && $folder->isInDB()) ? $folder->Title : _t('AssetAdmin.FILES', 'Files'); |
||
146 | $fields->push(new HiddenField('ID', false, $folder ? $folder->ID : null)); |
||
147 | |||
148 | // File listing |
||
149 | $gridFieldConfig = GridFieldConfig::create()->addComponents( |
||
150 | new GridFieldToolbarHeader(), |
||
151 | new GridFieldSortableHeader(), |
||
152 | new GridFieldFilterHeader(), |
||
153 | new GridFieldDataColumns(), |
||
154 | new GridFieldPaginator(self::config()->page_length), |
||
155 | new GridFieldEditButton(), |
||
156 | new GridFieldDeleteAction(), |
||
157 | new GridFieldDetailForm(), |
||
158 | GridFieldLevelup::create($folder->ID)->setLinkSpec('admin/assets/show/%d') |
||
159 | ); |
||
160 | |||
161 | $gridField = GridField::create('File', $title, $this->getList(), $gridFieldConfig); |
||
162 | $columns = $gridField->getConfig()->getComponentByType('GridFieldDataColumns'); |
||
163 | $columns->setDisplayFields(array( |
||
164 | 'StripThumbnail' => '', |
||
165 | 'Title' => _t('File.Title', 'Title'), |
||
166 | 'Created' => _t('AssetAdmin.CREATED', 'Date'), |
||
167 | 'Size' => _t('AssetAdmin.SIZE', 'Size'), |
||
168 | )); |
||
169 | $columns->setFieldCasting(array( |
||
170 | 'Created' => 'SS_Datetime->Nice' |
||
171 | )); |
||
172 | $gridField->setAttribute( |
||
173 | 'data-url-folder-template', |
||
174 | Controller::join_links($this->Link('show'), '%s') |
||
175 | ); |
||
176 | |||
177 | if(!$folder->hasMethod('canAddChildren') || ($folder->hasMethod('canAddChildren') && $folder->canAddChildren())) { |
||
178 | // TODO Will most likely be replaced by GridField logic |
||
179 | $addFolderBtn = new LiteralField( |
||
180 | 'AddFolderButton', |
||
181 | sprintf( |
||
182 | '<a class="ss-ui-button font-icon-folder-add no-text cms-add-folder-link" title="%s" data-icon="add" data-url="%s" href="%s"></a>', |
||
183 | _t('Folder.AddFolderButton', 'Add folder'), |
||
184 | Controller::join_links($this->Link('AddForm'), '?' . http_build_query(array( |
||
185 | 'action_doAdd' => 1, |
||
186 | 'ParentID' => $folder->ID, |
||
187 | 'SecurityID' => $form->getSecurityToken()->getValue() |
||
188 | ))), |
||
189 | Controller::join_links($this->Link('addfolder'), '?ParentID=' . $folder->ID) |
||
190 | ) |
||
191 | ); |
||
192 | } else { |
||
193 | $addFolderBtn = ''; |
||
194 | } |
||
195 | |||
196 | // Move existing fields to a "details" tab, unless they've already been tabbed out through extensions. |
||
197 | // Required to keep Folder->getCMSFields() simple and reuseable, |
||
198 | // without any dependencies into AssetAdmin (e.g. useful for "add folder" views). |
||
199 | if(!$fields->hasTabset()) { |
||
200 | $tabs = new TabSet('Root', |
||
201 | $tabList = new Tab('ListView', _t('AssetAdmin.ListView', 'List View')), |
||
202 | $tabTree = new Tab('TreeView', _t('AssetAdmin.TreeView', 'Tree View')) |
||
203 | ); |
||
204 | $tabList->addExtraClass("content-listview cms-tabset-icon list"); |
||
205 | $tabTree->addExtraClass("content-treeview cms-tabset-icon tree"); |
||
206 | if($fields->Count() && $folder && $folder->isInDB()) { |
||
207 | $tabs->push($tabDetails = new Tab('DetailsView', _t('AssetAdmin.DetailsView', 'Details'))); |
||
208 | $tabDetails->addExtraClass("content-galleryview cms-tabset-icon edit"); |
||
209 | foreach($fields as $field) { |
||
210 | $fields->removeByName($field->getName()); |
||
211 | $tabDetails->push($field); |
||
212 | } |
||
213 | } |
||
214 | $fields->push($tabs); |
||
215 | } |
||
216 | |||
217 | // we only add buttons if they're available. User might not have permission and therefore |
||
218 | // the button shouldn't be available. Adding empty values into a ComposteField breaks template rendering. |
||
219 | $actionButtonsComposite = CompositeField::create()->addExtraClass('cms-actions-row'); |
||
220 | if($addFolderBtn) $actionButtonsComposite->push($addFolderBtn); |
||
221 | |||
222 | // Add the upload field for new media |
||
223 | if($currentPageID = $this->currentPageID()){ |
||
224 | Session::set("{$this->class}.currentPage", $currentPageID); |
||
225 | } |
||
226 | |||
227 | $folder = $this->currentPage(); |
||
228 | |||
229 | $uploadField = UploadField::create('AssetUploadField', ''); |
||
230 | $uploadField->setConfig('previewMaxWidth', 40); |
||
231 | $uploadField->setConfig('previewMaxHeight', 30); |
||
232 | $uploadField->setConfig('changeDetection', false); |
||
233 | $uploadField->addExtraClass('ss-assetuploadfield'); |
||
234 | $uploadField->removeExtraClass('ss-uploadfield'); |
||
235 | $uploadField->setTemplate('AssetUploadField'); |
||
236 | |||
237 | if($folder->exists()) { |
||
238 | $path = $folder->getFilename(); |
||
239 | $uploadField->setFolderName($path); |
||
240 | } else { |
||
241 | $uploadField->setFolderName('/'); // root of the assets |
||
242 | } |
||
243 | |||
244 | $exts = $uploadField->getValidator()->getAllowedExtensions(); |
||
245 | asort($exts); |
||
246 | $uploadField->Extensions = implode(', ', $exts); |
||
247 | |||
248 | // List view |
||
249 | $fields->addFieldsToTab('Root.ListView', array( |
||
250 | $actionsComposite = CompositeField::create( |
||
251 | $actionButtonsComposite |
||
252 | )->addExtraClass('cms-content-toolbar field'), |
||
253 | $uploadField, |
||
254 | new HiddenField('ID'), |
||
255 | $gridField |
||
256 | )); |
||
257 | |||
258 | // Tree view |
||
259 | $fields->addFieldsToTab('Root.TreeView', array( |
||
260 | clone $actionsComposite, |
||
261 | // TODO Replace with lazy loading on client to avoid performance hit of rendering potentially unused views |
||
262 | new LiteralField( |
||
263 | 'Tree', |
||
264 | FormField::create_tag( |
||
265 | 'div', |
||
266 | array( |
||
267 | 'class' => 'cms-tree', |
||
268 | 'data-url-tree' => $this->Link('getsubtree'), |
||
269 | 'data-url-savetreenode' => $this->Link('savetreenode') |
||
270 | ), |
||
271 | $this->SiteTreeAsUL() |
||
272 | ) |
||
273 | ) |
||
274 | )); |
||
275 | |||
276 | // Move actions to "details" tab (they don't make sense on list/tree view) |
||
277 | $actions = $form->Actions(); |
||
278 | $saveBtn = $actions->fieldByName('action_save'); |
||
279 | $deleteBtn = $actions->fieldByName('action_delete'); |
||
280 | $actions->removeByName('action_save'); |
||
281 | $actions->removeByName('action_delete'); |
||
282 | if(($saveBtn || $deleteBtn) && $fields->fieldByName('Root.DetailsView')) { |
||
283 | $fields->addFieldToTab( |
||
284 | 'Root.DetailsView', |
||
285 | CompositeField::create($saveBtn,$deleteBtn)->addExtraClass('Actions') |
||
286 | ); |
||
287 | } |
||
288 | |||
289 | |||
290 | $fields->setForm($form); |
||
291 | $form->setTemplate($this->getTemplatesWithSuffix('_EditForm')); |
||
292 | // TODO Can't merge $FormAttributes in template at the moment |
||
293 | $form->addExtraClass('cms-edit-form ' . $this->BaseCSSClasses()); |
||
294 | $form->setAttribute('data-pjax-fragment', 'CurrentForm'); |
||
295 | $form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet'); |
||
296 | |||
297 | // Optionally handle form submissions with 'X-Formschema-Request' |
||
298 | // which rely on having validation errors returned as structured data |
||
299 | $form->setValidationResponseCallback(function() use ($form) { |
||
300 | $request = $this->getRequest(); |
||
301 | if($request->getHeader('X-Formschema-Request')) { |
||
302 | $data = $this->getSchemaForForm($form); |
||
303 | $response = new SS_HTTPResponse(Convert::raw2json($data)); |
||
304 | $response->addHeader('Content-Type', 'application/json'); |
||
305 | return $response; |
||
306 | |||
307 | } |
||
308 | }); |
||
309 | |||
310 | |||
311 | $this->extend('updateEditForm', $form); |
||
312 | |||
313 | return $form; |
||
314 | } |
||
315 | |||
316 | public function addfolder($request) { |
||
317 | $obj = $this->customise(array( |
||
318 | 'EditForm' => $this->AddForm() |
||
319 | )); |
||
320 | |||
321 | if($request->isAjax()) { |
||
322 | // Rendering is handled by template, which will call EditForm() eventually |
||
323 | $content = $obj->renderWith($this->getTemplatesWithSuffix('_Content')); |
||
324 | } else { |
||
325 | $content = $obj->renderWith($this->getViewer('show')); |
||
326 | } |
||
327 | |||
328 | return $content; |
||
329 | } |
||
330 | |||
331 | public function delete($data, $form) { |
||
332 | $className = $this->stat('tree_class'); |
||
333 | |||
334 | $record = DataObject::get_by_id($className, $data['ID']); |
||
335 | if($record && !$record->canDelete()) { |
||
336 | return Security::permissionFailure(); |
||
337 | } |
||
338 | View Code Duplication | if(!$record || !$record->ID) { |
|
339 | throw new SS_HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404); |
||
340 | } |
||
341 | $parentID = $record->ParentID; |
||
342 | $record->delete(); |
||
343 | $this->setCurrentPageID(null); |
||
344 | |||
345 | $this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.DELETED', 'Deleted.'))); |
||
346 | $this->getResponse()->addHeader('X-Pjax', 'Content'); |
||
347 | return $this->redirect(Controller::join_links($this->Link('show'), $parentID ? $parentID : 0)); |
||
348 | } |
||
349 | |||
350 | /** |
||
351 | * Get the search context |
||
352 | * |
||
353 | * @return SearchContext |
||
354 | */ |
||
355 | public function getSearchContext() { |
||
403 | |||
404 | /** |
||
405 | * Returns a form for filtering of files and assets gridfield. |
||
406 | * Result filtering takes place in {@link getList()}. |
||
407 | * |
||
408 | * @return Form |
||
409 | * @see AssetAdmin.js |
||
410 | */ |
||
411 | public function SearchForm() { |
||
432 | |||
433 | public function AddForm() { |
||
467 | |||
468 | /** |
||
469 | * Add a new group and return its details suitable for ajax. |
||
470 | * |
||
471 | * @todo Move logic into Folder class, and use LeftAndMain->doAdd() default implementation. |
||
472 | */ |
||
473 | public function doAdd($data, $form) { |
||
530 | |||
531 | /** |
||
532 | * Get an asset renamer for the given filename. |
||
533 | * |
||
534 | * @param string $filename Path name |
||
535 | * @return AssetNameGenerator |
||
536 | */ |
||
537 | protected function getNameGenerator($filename){ |
||
541 | |||
542 | /** |
||
543 | * Custom currentPage() method to handle opening the 'root' folder |
||
544 | */ |
||
545 | public function currentPage() { |
||
556 | |||
557 | public function getSiteTreeFor($className, $rootID = null, $childrenMethod = null, $numChildrenMethod = null, $filterFunction = null, $minNodeCount = 30) { |
||
562 | |||
563 | public function getCMSTreeTitle() { |
||
566 | |||
567 | public function SiteTreeAsUL() { |
||
570 | |||
571 | /** |
||
572 | * @param bool $unlinked |
||
573 | * @return ArrayList |
||
574 | */ |
||
575 | public function Breadcrumbs($unlinked = false) { |
||
601 | |||
602 | public function providePermissions() { |
||
611 | |||
612 | } |
||
613 | /** |
||
647 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.