Total Complexity | 108 |
Total Lines | 603 |
Duplicated Lines | 0 % |
Changes | 9 | ||
Bugs | 0 | Features | 0 |
Complex classes like RecordsRequestHandler 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 RecordsRequestHandler, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
31 | abstract class RecordsRequestHandler extends RequestHandler |
||
32 | { |
||
33 | public $config; |
||
34 | |||
35 | public static $recordClass; |
||
36 | public $accountLevelRead = false; |
||
37 | public $accountLevelBrowse = 'Staff'; |
||
38 | public $accountLevelWrite = 'Staff'; |
||
39 | public $accountLevelAPI = false; |
||
40 | public $browseOrder = false; |
||
41 | public $browseConditions = false; |
||
42 | public $browseLimitDefault = false; |
||
43 | public $editableFields = false; |
||
44 | public $searchConditions = false; |
||
45 | public $calledClass = __CLASS__; |
||
46 | |||
47 | public function __construct() |
||
48 | { |
||
49 | $this->responseBuilder = TwigBuilder::class; |
||
50 | } |
||
51 | |||
52 | /** |
||
53 | * Start of routing for this controller. |
||
54 | * Methods in this execution path will always respond either as an error or a normal response. |
||
55 | * Responsible for detecting JSON or JSONP response modes. |
||
56 | * |
||
57 | * @return void |
||
58 | */ |
||
59 | public function handle(ServerRequestInterface $request): ResponseInterface |
||
60 | { |
||
61 | // save static class |
||
62 | $this->calledClass = get_called_class(); |
||
63 | |||
64 | // handle JSON requests |
||
65 | if ($this->peekPath() == 'json') { |
||
66 | $this->shiftPath(); |
||
67 | |||
68 | // check access for API response modes |
||
69 | $this->responseBuilder = JsonBuilder::class; |
||
70 | |||
71 | if (in_array($this->responseBuilder, [JsonBuilder::class,JsonpBuilder::class])) { |
||
72 | if (!$this->checkAPIAccess()) { |
||
73 | return $this->throwAPIUnAuthorizedError(); |
||
|
|||
74 | } |
||
75 | } |
||
76 | } |
||
77 | |||
78 | return $this->handleRecordsRequest(); |
||
79 | } |
||
80 | |||
81 | public function handleRecordsRequest($action = false): ResponseInterface |
||
82 | { |
||
83 | switch ($action ? $action : $action = $this->shiftPath()) { |
||
84 | case 'save': |
||
85 | { |
||
86 | return $this->handleMultiSaveRequest(); |
||
87 | } |
||
88 | |||
89 | case 'destroy': |
||
90 | { |
||
91 | return $this->handleMultiDestroyRequest(); |
||
92 | } |
||
93 | |||
94 | case 'create': |
||
95 | { |
||
96 | return $this->handleCreateRequest(); |
||
97 | } |
||
98 | |||
99 | case '': |
||
100 | case false: |
||
101 | { |
||
102 | return $this->handleBrowseRequest(); |
||
103 | } |
||
104 | |||
105 | default: |
||
106 | { |
||
107 | if ($Record = $this->getRecordByHandle($action)) { |
||
108 | return $this->handleRecordRequest($Record); |
||
109 | } else { |
||
110 | return $this->throwRecordNotFoundError(); |
||
111 | } |
||
112 | } |
||
113 | } |
||
114 | } |
||
115 | |||
116 | public function getRecordByHandle($handle) |
||
117 | { |
||
118 | $className = static::$recordClass; |
||
119 | if (method_exists($className, 'getByHandle')) { |
||
120 | return $className::getByHandle($handle); |
||
121 | } |
||
122 | } |
||
123 | |||
124 | public function prepareBrowseConditions($conditions = []) |
||
125 | { |
||
126 | if ($this->browseConditions) { |
||
127 | if (!is_array($this->browseConditions)) { |
||
128 | $this->browseConditions = [$this->browseConditions]; |
||
129 | } |
||
130 | $conditions = array_merge($this->browseConditions, $conditions); |
||
131 | } |
||
132 | return $conditions; |
||
133 | } |
||
134 | |||
135 | public function prepareDefaultBrowseOptions() |
||
136 | { |
||
137 | if (!isset($_REQUEST['offset'])) { |
||
138 | if (isset($_REQUEST['start'])) { |
||
139 | if (is_numeric($_REQUEST['start'])) { |
||
140 | $_REQUEST['offset'] = $_REQUEST['start']; |
||
141 | } |
||
142 | } |
||
143 | } |
||
144 | |||
145 | $limit = !empty($_REQUEST['limit']) && is_numeric($_REQUEST['limit']) ? $_REQUEST['limit'] : $this->browseLimitDefault; |
||
146 | $offset = !empty($_REQUEST['offset']) && is_numeric($_REQUEST['offset']) ? $_REQUEST['offset'] : false; |
||
147 | |||
148 | $options = [ |
||
149 | 'limit' => $limit, |
||
150 | 'offset' => $offset, |
||
151 | 'order' => $this->browseOrder, |
||
152 | ]; |
||
153 | |||
154 | return $options; |
||
155 | } |
||
156 | |||
157 | public function handleBrowseRequest($options = [], $conditions = [], $responseID = null, $responseData = []) |
||
158 | { |
||
159 | if (!$this->checkBrowseAccess(func_get_args())) { |
||
160 | return $this->throwUnauthorizedError(); |
||
161 | } |
||
162 | |||
163 | $conditions = $this->prepareBrowseConditions($conditions); |
||
164 | |||
165 | $options = $this->prepareDefaultBrowseOptions(); |
||
166 | |||
167 | // process sorter |
||
168 | if (!empty($_REQUEST['sort'])) { |
||
169 | $sort = json_decode($_REQUEST['sort'], true); |
||
170 | if (!$sort || !is_array($sort)) { |
||
171 | return $this->respond('error', [ |
||
172 | 'success' => false, |
||
173 | 'failed' => [ |
||
174 | 'errors' => 'Invalid sorter.', |
||
175 | ], |
||
176 | ]); |
||
177 | } |
||
178 | |||
179 | if (is_array($sort)) { |
||
180 | foreach ($sort as $field) { |
||
181 | $options['order'][$field['property']] = $field['direction']; |
||
182 | } |
||
183 | } |
||
184 | } |
||
185 | |||
186 | // process filter |
||
187 | if (!empty($_REQUEST['filter'])) { |
||
188 | $filter = json_decode($_REQUEST['filter'], true); |
||
189 | if (!$filter || !is_array($filter)) { |
||
190 | return $this->respond('error', [ |
||
191 | 'success' => false, |
||
192 | 'failed' => [ |
||
193 | 'errors' => 'Invalid filter.', |
||
194 | ], |
||
195 | ]); |
||
196 | } |
||
197 | |||
198 | foreach ($filter as $field) { |
||
199 | $conditions[$field['property']] = $field['value']; |
||
200 | } |
||
201 | } |
||
202 | |||
203 | $className = static::$recordClass; |
||
204 | |||
205 | return $this->respond( |
||
206 | isset($responseID) ? $responseID : $this->getTemplateName($className::$pluralNoun), |
||
207 | array_merge($responseData, [ |
||
208 | 'success' => true, |
||
209 | 'data' => $className::getAllByWhere($conditions, $options), |
||
210 | 'conditions' => $conditions, |
||
211 | 'total' => DB::foundRows(), |
||
212 | 'limit' => $options['limit'], |
||
213 | 'offset' => $options['offset'], |
||
214 | ]) |
||
215 | ); |
||
216 | } |
||
217 | |||
218 | |||
219 | public function handleRecordRequest(ActiveRecord $Record, $action = false) |
||
220 | { |
||
221 | if (!$this->checkReadAccess($Record)) { |
||
222 | return $this->throwUnauthorizedError(); |
||
223 | } |
||
224 | |||
225 | switch ($action ? $action : $action = $this->shiftPath()) { |
||
226 | case '': |
||
227 | case false: |
||
228 | { |
||
229 | $className = static::$recordClass; |
||
230 | |||
231 | return $this->respond($this->getTemplateName($className::$singularNoun), [ |
||
232 | 'success' => true, |
||
233 | 'data' => $Record, |
||
234 | ]); |
||
235 | } |
||
236 | |||
237 | case 'edit': |
||
238 | { |
||
239 | return $this->handleEditRequest($Record); |
||
240 | } |
||
241 | |||
242 | case 'delete': |
||
243 | { |
||
244 | return $this->handleDeleteRequest($Record); |
||
245 | } |
||
246 | |||
247 | default: |
||
248 | { |
||
249 | return $this->onRecordRequestNotHandled($Record, $action); |
||
250 | } |
||
251 | } |
||
252 | } |
||
253 | |||
254 | |||
255 | public function prepareResponseModeJSON($methods = []) |
||
256 | { |
||
257 | if ($this->responseBuilder === JsonBuilder::class && in_array($_SERVER['REQUEST_METHOD'], $methods)) { |
||
258 | $JSONData = JSON::getRequestData(); |
||
259 | if (is_array($JSONData)) { |
||
260 | $_REQUEST = $JSONData; |
||
261 | } |
||
262 | } |
||
263 | } |
||
264 | |||
265 | public function getDatumRecord($datum) |
||
266 | { |
||
267 | $className = static::$recordClass; |
||
268 | $PrimaryKey = $className::getPrimaryKey(); |
||
269 | if (empty($datum[$PrimaryKey])) { |
||
270 | $record = new $className::$defaultClass(); |
||
271 | $this->onRecordCreated($record, $datum); |
||
272 | } else { |
||
273 | if (!$record = $className::getByID($datum[$PrimaryKey])) { |
||
274 | throw new Exception('Record not found'); |
||
275 | } |
||
276 | } |
||
277 | return $record; |
||
278 | } |
||
279 | |||
280 | public function processDatumSave($datum) |
||
281 | { |
||
282 | // get record |
||
283 | $Record = $this->getDatumRecord($datum); |
||
284 | |||
285 | // check write access |
||
286 | if (!$this->checkWriteAccess($Record)) { |
||
287 | throw new Exception('Write access denied'); |
||
288 | } |
||
289 | |||
290 | // apply delta |
||
291 | $this->applyRecordDelta($Record, $datum); |
||
292 | |||
293 | // call template function |
||
294 | $this->onBeforeRecordValidated($Record, $datum); |
||
295 | |||
296 | // try to save record |
||
297 | try { |
||
298 | // call template function |
||
299 | $this->onBeforeRecordSaved($Record, $datum); |
||
300 | |||
301 | $Record->save(); |
||
302 | |||
303 | // call template function |
||
304 | $this->onRecordSaved($Record, $datum); |
||
305 | |||
306 | return (!$Record::fieldExists('Class') || get_class($Record) == $Record->Class) ? $Record : $Record->changeClass(); |
||
307 | } catch (Exception $e) { |
||
308 | throw $e; |
||
309 | } |
||
310 | } |
||
311 | |||
312 | public function handleMultiSaveRequest(): ResponseInterface |
||
313 | { |
||
314 | $className = static::$recordClass; |
||
315 | |||
316 | $this->prepareResponseModeJSON(['POST','PUT']); |
||
317 | |||
318 | if ($className::fieldExists(key($_REQUEST['data']))) { |
||
319 | $_REQUEST['data'] = [$_REQUEST['data']]; |
||
320 | } |
||
321 | |||
322 | if (empty($_REQUEST['data']) || !is_array($_REQUEST['data'])) { |
||
323 | return $this->respond('error', [ |
||
324 | 'success' => false, |
||
325 | 'failed' => [ |
||
326 | 'errors' => 'Save expects "data" field as array of records.', |
||
327 | ], |
||
328 | ]); |
||
329 | } |
||
330 | |||
331 | $results = []; |
||
332 | $failed = []; |
||
333 | |||
334 | foreach ($_REQUEST['data'] as $datum) { |
||
335 | try { |
||
336 | $results[] = $this->processDatumSave($datum); |
||
337 | } catch (Exception $e) { |
||
338 | $failed[] = [ |
||
339 | 'record' => $datum, |
||
340 | 'errors' => $e->getMessage(), |
||
341 | ]; |
||
342 | continue; |
||
343 | } |
||
344 | } |
||
345 | |||
346 | |||
347 | return $this->respond($this->getTemplateName($className::$pluralNoun).'Saved', [ |
||
348 | 'success' => count($results) || !count($failed), |
||
349 | 'data' => $results, |
||
350 | 'failed' => $failed, |
||
351 | ]); |
||
352 | } |
||
353 | |||
354 | public function processDatumDestroy($datum) |
||
355 | { |
||
356 | $className = static::$recordClass; |
||
357 | $PrimaryKey = $className::getPrimaryKey(); |
||
358 | |||
359 | // get record |
||
360 | if (is_numeric($datum)) { |
||
361 | $recordID = $datum; |
||
362 | } elseif (!empty($datum[$PrimaryKey]) && is_numeric($datum[$PrimaryKey])) { |
||
363 | $recordID = $datum[$PrimaryKey]; |
||
364 | } else { |
||
365 | throw new Exception($PrimaryKey.' missing'); |
||
366 | } |
||
367 | |||
368 | if (!$Record = $className::getByField($PrimaryKey, $recordID)) { |
||
369 | throw new Exception($PrimaryKey.' not found'); |
||
370 | } |
||
371 | |||
372 | // check write access |
||
373 | if (!$this->checkWriteAccess($Record)) { |
||
374 | throw new Exception('Write access denied'); |
||
375 | } |
||
376 | |||
377 | if ($Record->destroy()) { |
||
378 | return $Record; |
||
379 | } else { |
||
380 | throw new Exception('Destroy failed'); |
||
381 | } |
||
382 | } |
||
383 | |||
384 | public function handleMultiDestroyRequest(): ResponseInterface |
||
422 | ]); |
||
423 | } |
||
424 | |||
425 | |||
426 | public function handleCreateRequest(ActiveRecord $Record = null): ResponseInterface |
||
427 | { |
||
428 | // save static class |
||
429 | $this->calledClass = get_called_class(); |
||
430 | |||
431 | if (!$Record) { |
||
432 | $className = static::$recordClass; |
||
433 | $Record = new $className::$defaultClass(); |
||
434 | } |
||
435 | |||
436 | // call template function |
||
437 | $this->onRecordCreated($Record, $_REQUEST); |
||
438 | |||
439 | return $this->handleEditRequest($Record); |
||
440 | } |
||
441 | |||
442 | public function handleEditRequest(ActiveRecord $Record): ResponseInterface |
||
443 | { |
||
444 | $className = static::$recordClass; |
||
445 | |||
446 | if (!$this->checkWriteAccess($Record)) { |
||
447 | return $this->throwUnauthorizedError(); |
||
448 | } |
||
449 | |||
450 | if (in_array($_SERVER['REQUEST_METHOD'], ['POST','PUT'])) { |
||
451 | if ($this->responseBuilder === JsonBuilder::class) { |
||
452 | $_REQUEST = JSON::getRequestData(); |
||
453 | if (is_array($_REQUEST['data'])) { |
||
454 | $_REQUEST = $_REQUEST['data']; |
||
455 | } |
||
456 | } |
||
457 | $_REQUEST = $_REQUEST ? $_REQUEST : $_POST; |
||
458 | |||
459 | // apply delta |
||
460 | $this->applyRecordDelta($Record, $_REQUEST); |
||
461 | |||
462 | // call template function |
||
463 | $this->onBeforeRecordValidated($Record, $_REQUEST); |
||
464 | |||
465 | // validate |
||
466 | if ($Record->validate()) { |
||
467 | // call template function |
||
468 | $this->onBeforeRecordSaved($Record, $_REQUEST); |
||
469 | |||
470 | try { |
||
471 | // save session |
||
472 | $Record->save(); |
||
473 | } catch (Exception $e) { |
||
474 | return $this->respond('Error', [ |
||
475 | 'success' => false, |
||
476 | 'failed' => [ |
||
477 | 'errors' => $e->getMessage(), |
||
478 | ], |
||
479 | ]); |
||
480 | } |
||
481 | |||
482 | // call template function |
||
483 | $this->onRecordSaved($Record, $_REQUEST); |
||
484 | |||
485 | // fire created response |
||
486 | $responseID = $this->getTemplateName($className::$singularNoun).'Saved'; |
||
487 | $responseData = [ |
||
488 | 'success' => true, |
||
489 | 'data' => $Record, |
||
490 | ]; |
||
491 | return $this->respond($responseID, $responseData); |
||
492 | } |
||
493 | |||
494 | // fall through back to form if validation failed |
||
495 | } |
||
496 | |||
497 | $responseID = $this->getTemplateName($className::$singularNoun).'Edit'; |
||
498 | $responseData = [ |
||
499 | 'success' => false, |
||
500 | 'data' => $Record, |
||
501 | ]; |
||
502 | |||
503 | return $this->respond($responseID, $responseData); |
||
504 | } |
||
505 | |||
506 | |||
507 | public function handleDeleteRequest(ActiveRecord $Record): ResponseInterface |
||
508 | { |
||
509 | $className = static::$recordClass; |
||
510 | |||
511 | if (!$this->checkWriteAccess($Record)) { |
||
512 | return $this->throwUnauthorizedError(); |
||
513 | } |
||
514 | |||
515 | if ($_SERVER['REQUEST_METHOD'] == 'POST') { |
||
516 | $data = $Record->data; |
||
517 | $Record->destroy(); |
||
518 | |||
519 | // call cleanup function after delete |
||
520 | $this->onRecordDeleted($Record, $data); |
||
521 | |||
522 | // fire created response |
||
523 | return $this->respond($this->getTemplateName($className::$singularNoun).'Deleted', [ |
||
524 | 'success' => true, |
||
525 | 'data' => $Record, |
||
526 | ]); |
||
527 | } |
||
528 | |||
529 | return $this->respond('confirm', [ |
||
530 | 'question' => 'Are you sure you want to delete this '.$className::$singularNoun.'?', |
||
531 | 'data' => $Record, |
||
532 | ]); |
||
533 | } |
||
534 | |||
535 | // access control template functions |
||
536 | public function checkBrowseAccess($arguments) |
||
537 | { |
||
538 | return true; |
||
539 | } |
||
540 | |||
541 | public function checkReadAccess(ActiveRecord $Record) |
||
542 | { |
||
543 | return true; |
||
544 | } |
||
545 | |||
546 | public function checkWriteAccess(ActiveRecord $Record) |
||
549 | } |
||
550 | |||
551 | public function checkAPIAccess() |
||
552 | { |
||
553 | return true; |
||
554 | } |
||
555 | |||
556 | public function throwUnauthorizedError() |
||
557 | { |
||
558 | return $this->respond('Unauthorized', [ |
||
559 | 'success' => false, |
||
560 | 'failed' => [ |
||
561 | 'errors' => 'Login required.', |
||
562 | ], |
||
563 | ]); |
||
564 | } |
||
565 | |||
566 | public function throwAPIUnAuthorizedError() |
||
567 | { |
||
568 | return $this->respond('Unauthorized', [ |
||
569 | 'success' => false, |
||
570 | 'failed' => [ |
||
571 | 'errors' => 'API access required.', |
||
572 | ], |
||
573 | ]); |
||
574 | } |
||
575 | |||
576 | public function throwNotFoundError() |
||
577 | { |
||
578 | return $this->respond('error', [ |
||
579 | 'success' => false, |
||
580 | 'failed' => [ |
||
581 | 'errors' => 'Record not found.', |
||
582 | ], |
||
583 | ]); |
||
584 | } |
||
585 | |||
586 | public function onRecordRequestNotHandled(ActiveRecord $Record, $action) |
||
587 | { |
||
588 | return $this->respond('error', [ |
||
589 | 'success' => false, |
||
590 | 'failed' => [ |
||
591 | 'errors' => 'Malformed request.', |
||
592 | ], |
||
593 | ]); |
||
594 | } |
||
595 | |||
596 | |||
597 | |||
598 | public function getTemplateName($noun) |
||
599 | { |
||
600 | return preg_replace_callback('/\s+([a-zA-Z])/', function ($matches) { |
||
601 | return strtoupper($matches[1]); |
||
602 | }, $noun); |
||
603 | } |
||
604 | |||
605 | public function applyRecordDelta(ActiveRecord $Record, $data) |
||
606 | { |
||
607 | if (is_array($this->editableFields)) { |
||
608 | $Record->setFields(array_intersect_key($data, array_flip($this->editableFields))); |
||
609 | } else { |
||
610 | $Record->setFields($data); |
||
611 | } |
||
612 | } |
||
613 | |||
614 | // event template functions |
||
615 | protected function onRecordCreated(ActiveRecord $Record, $data) |
||
616 | { |
||
617 | } |
||
618 | protected function onBeforeRecordValidated(ActiveRecord $Record, $data) |
||
619 | { |
||
620 | } |
||
621 | protected function onBeforeRecordSaved(ActiveRecord $Record, $data) |
||
623 | } |
||
624 | protected function onRecordDeleted(ActiveRecord $Record, $data) |
||
626 | } |
||
627 | protected function onRecordSaved(ActiveRecord $Record, $data) |
||
628 | { |
||
629 | } |
||
630 | |||
631 | protected function throwRecordNotFoundError() |
||
632 | { |
||
633 | return $this->throwNotFoundError(); |
||
634 | } |
||
635 | } |
||
636 |