| Total Complexity | 70 | 
| Total Lines | 461 | 
| Duplicated Lines | 0 % | 
| Changes | 0 | ||
Complex classes like RegistryPageController 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.
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 RegistryPageController, and based on these observations, apply Extract Interface, too.
| 1 | <?php  | 
            ||
| 22 | class RegistryPageController extends PageController  | 
            ||
| 23 | { | 
            ||
| 24 | private static $allowed_actions = [  | 
            ||
| 25 | 'RegistryFilterForm',  | 
            ||
| 26 | 'show',  | 
            ||
| 27 | 'export',  | 
            ||
| 28 | ];  | 
            ||
| 29 | |||
| 30 | /**  | 
            ||
| 31 | * Whether to output headers when sending the export file. This can be disabled for example in unit tests.  | 
            ||
| 32 | *  | 
            ||
| 33 | * @config  | 
            ||
| 34 | * @var bool  | 
            ||
| 35 | */  | 
            ||
| 36 | private static $output_headers = true;  | 
            ||
| 37 | |||
| 38 | /**  | 
            ||
| 39 | * Get all search query vars, compiled into a query string for a URL.  | 
            ||
| 40 | * This will escape all the variables to avoid XSS.  | 
            ||
| 41 | *  | 
            ||
| 42 | * @return string  | 
            ||
| 43 | */  | 
            ||
| 44 | public function AllQueryVars()  | 
            ||
| 45 |     { | 
            ||
| 46 | return Convert::raw2xml(http_build_query($this->queryVars()));  | 
            ||
| 47 | }  | 
            ||
| 48 | |||
| 49 | /**  | 
            ||
| 50 | * Get all search query vars except Sort and Dir, compiled into a query link.  | 
            ||
| 51 | * This will escape all the variables to avoid XSS.  | 
            ||
| 52 | *  | 
            ||
| 53 | * @return string  | 
            ||
| 54 | */  | 
            ||
| 55 | public function QueryLink()  | 
            ||
| 56 |     { | 
            ||
| 57 | $vars = $this->queryVars();  | 
            ||
| 58 | unset($vars['Sort']);  | 
            ||
| 59 | unset($vars['Dir']);  | 
            ||
| 60 | |||
| 61 |         return Convert::raw2xml($this->Link('RegistryFilterForm') . '?' . http_build_query($vars)); | 
            ||
| 62 | }  | 
            ||
| 63 | |||
| 64 | public function Sort()  | 
            ||
| 67 | }  | 
            ||
| 68 | |||
| 69 | /**  | 
            ||
| 70 | * Return the opposite direction from the currently sorted column's direction.  | 
            ||
| 71 | * @return string  | 
            ||
| 72 | */  | 
            ||
| 73 | public function OppositeDirection()  | 
            ||
| 91 | }  | 
            ||
| 92 | |||
| 93 | public function RegistryFilterForm()  | 
            ||
| 94 |     { | 
            ||
| 95 | $singleton = $this->dataRecord->getDataSingleton();  | 
            ||
| 96 |         if (!$singleton) { | 
            ||
| 97 | return;  | 
            ||
| 98 | }  | 
            ||
| 99 | |||
| 100 | $fields = $singleton->getSearchFields();  | 
            ||
| 101 | |||
| 102 | // Add the sort information.  | 
            ||
| 103 | $vars = $this->getRequest()->getVars();  | 
            ||
| 104 | $fields->merge(FieldList::create(  | 
            ||
| 105 |             HiddenField::create('Sort', 'Sort', (!$vars || empty($vars['Sort'])) ? 'ID' : $vars['Sort']), | 
            ||
| 106 |             HiddenField::create('Dir', 'Dir', (!$vars || empty($vars['Dir'])) ? 'ASC' : $vars['Dir']) | 
            ||
| 107 | ));  | 
            ||
| 108 | |||
| 109 | $actions = FieldList::create(  | 
            ||
| 110 |             FormAction::create('doRegistryFilter')->setTitle('Filter')->addExtraClass('btn btn-primary primary'), | 
            ||
| 111 |             FormAction::create('doRegistryFilterReset')->setTitle('Clear')->addExtraClass('btn') | 
            ||
| 112 | );  | 
            ||
| 113 | |||
| 114 | // Align vars to fields  | 
            ||
| 115 | $values = [];  | 
            ||
| 116 |         foreach ($this->getRequest()->getVars() as $field => $value) { | 
            ||
| 117 |             $values[str_replace('_', '.', $field)] = $value; | 
            ||
| 118 | }  | 
            ||
| 119 | |||
| 120 | $form = Form::create($this, 'RegistryFilterForm', $fields, $actions);  | 
            ||
| 121 | $form->loadDataFrom($values);  | 
            ||
| 122 | $form->disableSecurityToken();  | 
            ||
| 123 |         $form->setFormMethod('get'); | 
            ||
| 124 | |||
| 125 | return $form;  | 
            ||
| 126 | }  | 
            ||
| 127 | |||
| 128 | /**  | 
            ||
| 129 | * Build up search filters from user's search criteria and hand off  | 
            ||
| 130 |      * to the {@link query()} method to search against the database. | 
            ||
| 131 | *  | 
            ||
| 132 | * @param array $data Form request data  | 
            ||
| 133 | * @param Form Form object for submitted form  | 
            ||
| 134 | * @param HTTPRequest  | 
            ||
| 135 | * @return array  | 
            ||
| 136 | */  | 
            ||
| 137 | public function doRegistryFilter($data, $form, $request)  | 
            ||
| 138 |     { | 
            ||
| 139 | $singleton = $this->dataRecord->getDataSingleton();  | 
            ||
| 140 | |||
| 141 | // Restrict fields  | 
            ||
| 142 |         $fields = array_merge(['start', 'Sort', 'Dir'], $singleton->config()->get('searchable_fields')); | 
            ||
| 143 | $params = [];  | 
            ||
| 144 |         foreach ($fields as $field) { | 
            ||
| 145 |             $value = $this->getRequest()->getVar(str_replace('.', '_', $field)); | 
            ||
| 146 |             if ($value) { | 
            ||
| 147 | $params[$field] = $value;  | 
            ||
| 148 | }  | 
            ||
| 149 | }  | 
            ||
| 150 | |||
| 151 | // Link back to this page with the relevant parameters  | 
            ||
| 152 |         $this->redirect($this->Link('?' . http_build_query($params))); | 
            ||
| 153 | }  | 
            ||
| 154 | |||
| 155 | public function doRegistryFilterReset($data, $form, $request)  | 
            ||
| 156 |     { | 
            ||
| 157 | // Link back to this page with no relevant parameters.  | 
            ||
| 158 | $this->redirect($this->AbsoluteLink());  | 
            ||
| 159 | }  | 
            ||
| 160 | |||
| 161 | public function RegistryEntries($paginated = true)  | 
            ||
| 162 |     { | 
            ||
| 163 | |||
| 164 | $list = $this->queryList();  | 
            ||
| 165 | |||
| 166 |         if ($paginated) { | 
            ||
| 167 | $list = new PaginatedList($list, $this->getRequest());  | 
            ||
| 168 | $list->setPageLength($this->getPageLength());  | 
            ||
| 169 | }  | 
            ||
| 170 | |||
| 171 | return $list;  | 
            ||
| 172 | }  | 
            ||
| 173 | |||
| 174 | /**  | 
            ||
| 175 | * Loosely check if the record can be sorted by a property  | 
            ||
| 176 | * @param string $property  | 
            ||
| 177 | * @return boolean  | 
            ||
| 178 | */  | 
            ||
| 179 | public function canSortBy($property)  | 
            ||
| 180 |     { | 
            ||
| 181 | $canSort = false;  | 
            ||
| 182 | $singleton = $this->dataRecord->getDataSingleton();  | 
            ||
| 183 | |||
| 184 |         if ($singleton) { | 
            ||
| 185 |             $properties = explode('.', $property); | 
            ||
| 186 | |||
| 187 | $relationClass = $singleton->getRelationClass($properties[0]);  | 
            ||
| 188 |             if ($relationClass) { | 
            ||
| 189 |                 if (count($properties) <= 2 && singleton($relationClass)->hasDatabaseField($properties[1])) { | 
            ||
| 190 | $canSort = true;  | 
            ||
| 191 | }  | 
            ||
| 192 |             } elseif ($singleton instanceof DataObject) { | 
            ||
| 193 |                 if ($singleton->hasDatabaseField($property)) { | 
            ||
| 194 | $canSort = true;  | 
            ||
| 195 | }  | 
            ||
| 196 | }  | 
            ||
| 197 | }  | 
            ||
| 198 | |||
| 199 | return $canSort;  | 
            ||
| 200 | }  | 
            ||
| 201 | |||
| 202 | /**  | 
            ||
| 203 | * Format a set of columns, used for headings and row data  | 
            ||
| 204 | * @param int $id The result ID to reference  | 
            ||
| 205 | * @return ArrayList  | 
            ||
| 206 | */  | 
            ||
| 207 | public function Columns($id = null)  | 
            ||
| 208 |     { | 
            ||
| 209 | $singleton = $this->dataRecord->getDataSingleton();  | 
            ||
| 210 | $columns = $singleton->summaryFields();  | 
            ||
| 211 | $list = ArrayList::create();  | 
            ||
| 212 | $result = null;  | 
            ||
| 213 | |||
| 214 |         if ($id) { | 
            ||
| 215 | $result = $this->queryList()->byId($id);  | 
            ||
| 216 | }  | 
            ||
| 217 | |||
| 218 |         foreach ($columns as $name => $title) { | 
            ||
| 219 | // Check for unwanted parameters  | 
            ||
| 220 |             if (preg_match('/[()]/', $name)) { | 
            ||
| 221 | throw new RegistryException(_t(  | 
            ||
| 222 | 'SilverStripe\\Registry\\RegistryPageController.UNWANTEDCOLUMNPARAMETERS',  | 
            ||
| 223 | "Columns do not accept parameters"  | 
            ||
| 224 | ));  | 
            ||
| 225 | }  | 
            ||
| 226 | |||
| 227 | // Get dot deliniated properties  | 
            ||
| 228 |             $properties = explode('.', $name); | 
            ||
| 229 | |||
| 230 | // Increment properties for value  | 
            ||
| 231 | $context = $result;  | 
            ||
| 232 |             foreach ($properties as $property) { | 
            ||
| 233 |                 if ($context instanceof ViewableData) { | 
            ||
| 234 | $context = $context->obj($property);  | 
            ||
| 235 | }  | 
            ||
| 236 | }  | 
            ||
| 237 | |||
| 238 | // Check for link  | 
            ||
| 239 | $link = null;  | 
            ||
| 240 |             $useLink = $singleton->config()->get('use_link'); | 
            ||
| 241 |             if ($useLink !== false) { | 
            ||
| 242 |                 if ($result && $result->hasMethod('Link')) { | 
            ||
| 243 | $link = $result->Link();  | 
            ||
| 244 | }  | 
            ||
| 245 | }  | 
            ||
| 246 | |||
| 247 | // Format column  | 
            ||
| 248 | $list->push(ArrayData::create([  | 
            ||
| 249 | 'Name' => $name,  | 
            ||
| 250 | 'Title' => $title,  | 
            ||
| 251 | 'Link' => $link,  | 
            ||
| 252 | 'Value' => $context,  | 
            ||
| 253 | 'CanSort' => $this->canSortBy($name)  | 
            ||
| 254 | ]));  | 
            ||
| 255 | }  | 
            ||
| 256 | return $list;  | 
            ||
| 257 | }  | 
            ||
| 258 | |||
| 259 | /**  | 
            ||
| 260 | * Exports out all the data for the current search results.  | 
            ||
| 261 | * Sends the data to the browser as a CSV file.  | 
            ||
| 262 | */  | 
            ||
| 263 | public function export($request)  | 
            ||
| 264 |     { | 
            ||
| 265 | $dataClass = $this->dataRecord->getDataClass();  | 
            ||
| 266 | $resultColumns = $this->dataRecord->getDataSingleton()->fieldLabels();  | 
            ||
| 267 | |||
| 268 | // Used for the browser, not stored on the server  | 
            ||
| 269 |         $filepath = sprintf('export-%s.csv', date('Y-m-dHis')); | 
            ||
| 270 | |||
| 271 | // Allocates up to 1M of memory storage to write to, then will fail over to a temporary file on the filesystem  | 
            ||
| 272 |         $handle = fopen('php://temp/maxmemory:' . (1024 * 1024), 'w'); | 
            ||
| 273 | |||
| 274 | $cols = array_keys($resultColumns);  | 
            ||
| 275 | |||
| 276 | // put the headers in the first row  | 
            ||
| 277 | fputcsv($handle, $cols);  | 
            ||
| 278 | |||
| 279 | // put the data in the rows after  | 
            ||
| 280 |         foreach ($this->RegistryEntries(false) as $result) { | 
            ||
| 281 | $item = [];  | 
            ||
| 282 |             foreach ($cols as $col) { | 
            ||
| 283 | $item[] = $result->$col;  | 
            ||
| 284 | }  | 
            ||
| 285 | fputcsv($handle, $item);  | 
            ||
| 286 | }  | 
            ||
| 287 | |||
| 288 | rewind($handle);  | 
            ||
| 289 | |||
| 290 | // if the headers can't be sent (i.e. running a unit test, or something)  | 
            ||
| 291 | // just return the file path so the user can manually download the csv  | 
            ||
| 292 |         if (!headers_sent() && $this->config()->get('output_headers')) { | 
            ||
| 293 |             header('Content-Description: File Transfer'); | 
            ||
| 294 |             header('Content-Type: application/octet-stream'); | 
            ||
| 295 |             header('Content-Disposition: attachment; filename=' . $filepath); | 
            ||
| 296 |             header('Content-Transfer-Encoding: binary'); | 
            ||
| 297 |             header('Expires: 0'); | 
            ||
| 298 |             header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); | 
            ||
| 299 |             header('Pragma: public'); | 
            ||
| 300 |             header('Content-Length: ' . fstat($handle)['size']); | 
            ||
| 301 | ob_clean();  | 
            ||
| 302 | flush();  | 
            ||
| 303 | |||
| 304 | echo stream_get_contents($handle);  | 
            ||
| 305 | |||
| 306 | fclose($handle);  | 
            ||
| 307 |         } else { | 
            ||
| 308 | $contents = stream_get_contents($handle);  | 
            ||
| 309 | fclose($handle);  | 
            ||
| 310 | |||
| 311 | return $contents;  | 
            ||
| 312 | }  | 
            ||
| 313 | }  | 
            ||
| 314 | |||
| 315 | public function show($request)  | 
            ||
| 316 |     { | 
            ||
| 317 | // If Id is not numeric, then return an error page  | 
            ||
| 318 |         if (!is_numeric($request->param('ID'))) { | 
            ||
| 319 | return $this->httpError(404);  | 
            ||
| 320 | }  | 
            ||
| 321 | |||
| 322 |         $entry = DataObject::get_by_id($this->DataClass, $request->param('ID')); | 
            ||
| 323 | |||
| 324 |         if (!$entry || !$entry->exists()) { | 
            ||
| 325 | return $this->httpError(404);  | 
            ||
| 326 | }  | 
            ||
| 327 | |||
| 328 | return $this->customise([  | 
            ||
| 329 | 'Entry' => $entry  | 
            ||
| 330 | ]);  | 
            ||
| 331 | }  | 
            ||
| 332 | |||
| 333 | /**  | 
            ||
| 334 | * Perform a search against the data table.  | 
            ||
| 335 | * @return SS_List  | 
            ||
| 336 | */  | 
            ||
| 337 | protected function queryList()  | 
            ||
| 338 |     { | 
            ||
| 339 | // Sanity check  | 
            ||
| 340 | $dataClass = $this->dataRecord->getDataClass();  | 
            ||
| 341 |         if (!$dataClass) { | 
            ||
| 342 | return ArrayList::create();  | 
            ||
| 343 | }  | 
            ||
| 344 | |||
| 345 | // Setup  | 
            ||
| 346 | $singleton = $this->dataRecord->getDataSingleton();  | 
            ||
| 347 | |||
| 348 | // Create list  | 
            ||
| 349 | $list = $singleton->get();  | 
            ||
| 350 | |||
| 351 | // Setup filters  | 
            ||
| 352 | $filters = [];  | 
            ||
| 353 |         foreach ($singleton->config()->get('searchable_fields') as $field) { | 
            ||
| 354 |             $value = $this->getRequest()->getVar(str_replace('.', '_', $field)); | 
            ||
| 355 | |||
| 356 |             if ($value) { | 
            ||
| 357 | $filters[$field . ':PartialMatch'] = $value;  | 
            ||
| 358 | }  | 
            ||
| 359 | }  | 
            ||
| 360 | $list = $list->filter($filters);  | 
            ||
| 361 | |||
| 362 | // Sort  | 
            ||
| 363 |         $sort = $this->getRequest()->getVar('Sort'); | 
            ||
| 364 |         if ($sort) { | 
            ||
| 365 | $dir = 'ASC';  | 
            ||
| 366 |             if ($this->getRequest()->getVar('Dir')) { | 
            ||
| 367 |                 $dir = $this->getRequest()->getVar('Dir'); | 
            ||
| 368 | }  | 
            ||
| 369 | $list = $list->sort($sort, $dir);  | 
            ||
| 370 | }  | 
            ||
| 371 | |||
| 372 | return $list;  | 
            ||
| 373 | }  | 
            ||
| 374 | |||
| 375 | /**  | 
            ||
| 376 | * Safely escape a list of "select" candidates for a query  | 
            ||
| 377 | *  | 
            ||
| 378 | * @param array $names List of select fields  | 
            ||
| 379 | * @return array List of names, with each name double quoted  | 
            ||
| 380 | */  | 
            ||
| 381 | protected function escapeSelect($names)  | 
            ||
| 388 | );  | 
            ||
| 389 | }  | 
            ||
| 390 | |||
| 391 | /**  | 
            ||
| 392 | * Compiles all available GET variables for the result  | 
            ||
| 393 | * columns into an array. Used internally, not to be  | 
            ||
| 394 | * used directly with the templates or outside classes.  | 
            ||
| 395 | *  | 
            ||
| 396 | * This will NOT escape values to avoid XSS.  | 
            ||
| 397 | *  | 
            ||
| 398 | * @return array  | 
            ||
| 399 | */  | 
            ||
| 400 | protected function queryVars()  | 
            ||
| 424 | }  | 
            ||
| 425 | |||
| 426 | public function getTemplateList($action)  | 
            ||
| 427 |     { | 
            ||
| 428 | // Add action-specific templates for inheritance chain  | 
            ||
| 429 | $templates = [];  | 
            ||
| 430 | $parentClass = get_class($this);  | 
            ||
| 431 |         if ($action && $action !== 'index') { | 
            ||
| 432 | $parentClass = get_class($this);  | 
            ||
| 433 |             while ($parentClass !== Controller::class) { | 
            ||
| 434 | $templates[] = strtok($parentClass, '_') . '_' . $action;  | 
            ||
| 435 | $parentClass = get_parent_class($parentClass);  | 
            ||
| 436 | }  | 
            ||
| 437 | }  | 
            ||
| 438 | // Add controller templates for inheritance chain  | 
            ||
| 439 | $parentClass = get_class($this);  | 
            ||
| 440 |         while ($parentClass !== Controller::class) { | 
            ||
| 441 | $templates[] = strtok($parentClass, '_');  | 
            ||
| 442 | $parentClass = get_parent_class($parentClass);  | 
            ||
| 443 | }  | 
            ||
| 444 | |||
| 445 | $templates[] = Controller::class;  | 
            ||
| 446 | |||
| 447 | // remove duplicates  | 
            ||
| 448 | $templates = array_unique($templates);  | 
            ||
| 449 | |||
| 450 | $actionlessTemplates = [];  | 
            ||
| 451 | |||
| 452 |         if ($action && $action !== 'index') { | 
            ||
| 453 | array_unshift($templates, $this->DataClass . '_RegistryPage_' . $action);  | 
            ||
| 454 | }  | 
            ||
| 455 | array_unshift($actionlessTemplates, $this->DataClass . '_RegistryPage');  | 
            ||
| 456 | |||
| 457 | $parentClass = get_class($this->dataRecord);  | 
            ||
| 458 |         while ($parentClass !== RegistryPage::class) { | 
            ||
| 459 |             if ($action && $action != 'index') { | 
            ||
| 460 | array_unshift($templates, $parentClass . '_' . $action);  | 
            ||
| 461 | }  | 
            ||
| 462 | array_unshift($actionlessTemplates, $parentClass);  | 
            ||
| 463 | |||
| 464 | $parentClass = get_parent_class($parentClass);  | 
            ||
| 465 | }  | 
            ||
| 466 | |||
| 467 | $index = 0;  | 
            ||
| 468 |         while ($index < count($templates) && $templates[$index] !== RegistryPage::class) { | 
            ||
| 469 | $index++;  | 
            ||
| 470 | }  | 
            ||
| 471 | |||
| 472 | return array_merge(array_slice($templates, 0, $index), $actionlessTemplates, array_slice($templates, $index));  | 
            ||
| 473 | }  | 
            ||
| 474 | |||
| 475 | /**  | 
            ||
| 476 | * Sanitise a PHP class name for display in URLs etc  | 
            ||
| 477 | *  | 
            ||
| 478 | * @return string  | 
            ||
| 479 | */  | 
            ||
| 480 | public function getClassNameForUrl($className)  | 
            ||
| 483 | }  | 
            ||
| 484 | }  | 
            ||
| 485 | 
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths