Completed
Push — master ( 527622...a97140 )
by Henry
02:04
created

RecordsRequestHandler::processDatumSave()   C

Complexity

Conditions 7
Paths 17

Size

Total Lines 40
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 21
nc 17
nop 1
dl 0
loc 40
rs 6.7272
c 0
b 0
f 0
1
<?php
2
namespace Divergence\Controllers;
3
4
use Exception;
5
6
use Divergence\Helpers\JSON;
7
use Divergence\Helpers\JSONP;
8
use Divergence\Helpers\Util as Util;
9
use Divergence\IO\Database\MySQL as DB;
10
use Divergence\Models\ActiveRecord as ActiveRecord;
11
12
abstract class RecordsRequestHandler extends RequestHandler
13
{
14
    public static $config;
15
16
    // configurables
17
    public static $recordClass;
18
    public static $accountLevelRead = false;
19
    public static $accountLevelBrowse = 'Staff';
20
    public static $accountLevelWrite = 'Staff';
21
    public static $accountLevelAPI = false;
22
    public static $browseOrder = false;
23
    public static $browseConditions = false;
24
    public static $browseLimitDefault = false;
25
    public static $editableFields = false;
26
    public static $searchConditions = false;
27
    
28
    public static $calledClass = __CLASS__;
29
    public static $responseMode = 'dwoo';
30
    
31
    public static function handleRequest()
32
    {
33
        // save static class
34
        static::$calledClass = get_called_class();
35
    
36
        // handle JSON requests
37
        if (static::peekPath() == 'json') {
38
            // check access for API response modes
39
            static::$responseMode = static::shiftPath();
40
            if (in_array(static::$responseMode, ['json','jsonp'])) {
41
                if (!static::checkAPIAccess()) {
42
                    return static::throwAPIUnAuthorizedError();
43
                }
44
            }
45
        }
46
        
47
        return static::handleRecordsRequest();
48
    }
49
50
51
    public static function handleRecordsRequest($action = false)
52
    {
53
        switch ($action ? $action : $action = static::shiftPath()) {
54
            case 'save':
55
            {
56
                return static::handleMultiSaveRequest();
57
            }
58
            
59
            case 'destroy':
60
            {
61
                return static::handleMultiDestroyRequest();
62
            }
63
            
64
            case 'create':
65
            {
66
                return static::handleCreateRequest();
67
            }
68
            
69
            case '':
70
            case false:
71
            {
72
                return static::handleBrowseRequest();
73
            }
74
75
            default:
76
            {
77
                if ($Record = static::getRecordByHandle($action)) {
78
                    return static::handleRecordRequest($Record);
79
                } else {
80
                    return static::throwRecordNotFoundError();
81
                }
82
            }
83
        }
84
    }
85
    
86
    public static function getRecordByHandle($handle)
87
    {
88
        $className = static::$recordClass;
89
        
90
        if (method_exists($className, 'getByHandle')) {
91
            return $className::getByHandle($handle);
92
        }
93
    }
94
95
    public static function prepareBrowseConditions($conditions = [])
96
    {
97
        if (static::$browseConditions) {
98
            if (!is_array(static::$browseConditions)) {
99
                static::$browseConditions = [static::$browseConditions];
100
            }
101
            $conditions = array_merge(static::$browseConditions, $conditions);
102
        }
103
        return $conditions;
104
    }
105
106
    public static function prepareDefaultBrowseOptions()
107
    {
108
        if (empty($_REQUEST['offset']) && is_numeric($_REQUEST['start'])) {
109
            $_REQUEST['offset'] = $_REQUEST['start'];
110
        }
111
112
        $limit = !empty($_REQUEST['limit']) && is_numeric($_REQUEST['limit']) ? $_REQUEST['limit'] : static::$browseLimitDefault;
113
        $offset = !empty($_REQUEST['offset']) && is_numeric($_REQUEST['offset']) ? $_REQUEST['offset'] : false;
114
        
115
        $options = [
116
            'limit' =>  $limit,
117
            'offset' => $offset,
118
            'order' => static::$browseOrder,
119
        ];
120
121
        return $options;
122
    }
123
124
    public static function handleBrowseRequest($options = [], $conditions = [], $responseID = null, $responseData = [])
125
    {
126
        if (!static::checkBrowseAccess(func_get_args())) {
127
            return static::throwUnauthorizedError();
128
        }
129
        
130
        $conditions = static::prepareBrowseConditions($conditions);
131
        
132
        $options = static::prepareDefaultBrowseOptions();
133
134
        // process sorter
135
        if (!empty($_REQUEST['sort'])) {
136
            $sort = json_decode($_REQUEST['sort'], true);
137
            if (!$sort || !is_array($sort)) {
138
                return static::respond('error', [
139
                    'success' => false,
140
                    'failed' => [
141
                        'errors'	=>	'Invalid sorter.',
142
                    ],
143
                ]);
144
            }
145
146
            if (is_array($sort)) {
147
                foreach ($sort as $field) {
148
                    $options['order'][$field['property']] = $field['direction'];
149
                }
150
            }
151
        }
152
        
153
        // process filter
154
        if (!empty($_REQUEST['filter'])) {
155
            $filter = json_decode($_REQUEST['filter'], true);
156
            if (!$filter || !is_array($filter)) {
157
                return static::respond('error', [
158
                    'success' => false,
159
                    'failed' => [
160
                        'errors'	=>	'Invalid filter.',
161
                    ],
162
                ]);
163
            }
164
165
            foreach ($filter as $field) {
166
                $conditions[$field['property']] = $field['value'];
167
            }
168
        }
169
170
        $className = static::$recordClass;
171
172
        return static::respond(
173
            isset($responseID) ? $responseID : static::getTemplateName($className::$pluralNoun),
174
            array_merge($responseData, [
175
                'success' => true,
176
                'data' => $className::getAllByWhere($conditions, $options),
177
                'conditions' => $conditions,
178
                'total' => DB::foundRows(),
179
                'limit' => $options['limit'],
180
                'offset' => $options['offset'],
181
            ])
182
        );
183
    }
184
185
186
    public static function handleRecordRequest(ActiveRecord $Record, $action = false)
187
    {
188
        if (!static::checkReadAccess($Record)) {
189
            return static::throwUnauthorizedError();
190
        }
191
192
        switch ($action ? $action : $action = static::shiftPath()) {
193
            case '':
194
            case false:
195
            {
196
                $className = static::$recordClass;
197
                
198
                return static::respond(static::getTemplateName($className::$singularNoun), [
199
                    'success' => true,
200
                    'data' => $Record,
201
                ]);
202
            }
203
            
204
            case 'edit':
205
            {
206
                return static::handleEditRequest($Record);
207
            }
208
            
209
            case 'delete':
210
            {
211
                return static::handleDeleteRequest($Record);
212
            }
213
        
214
            default:
215
            {
216
                return static::onRecordRequestNotHandled($Record, $action);
217
            }
218
        }
219
    }
220
221
222
    public static function prepareResponseModeJSON($methods = [])
223
    {
224
        if (static::$responseMode == 'json' && in_array($_SERVER['REQUEST_METHOD'], $methods)) {
225
            $JSONData = JSON::getRequestData();
226
            if (is_array($JSONData)) {
227
                $_REQUEST = $JSONData;
228
            }
229
        }
230
    }
231
232
    public static function processDatumSave($datum)
233
    {
234
        $className = static::$recordClass;
235
        $PrimaryKey = $className::getPrimaryKey();
236
237
        // get record
238
        if (empty($datum[$PrimaryKey])) {
239
            $Record = new $className::$defaultClass();
240
            static::onRecordCreated($Record, $datum);
241
        } else {
242
            if (!$Record = $className::getByID($datum[$PrimaryKey])) {
243
                throw new Exception('Record not found');
244
            }
245
        }
246
        
247
        // check write access
248
        if (!static::checkWriteAccess($Record)) {
249
            throw new Exception('Write access denied');
250
        }
251
        
252
        // apply delta
253
        static::applyRecordDelta($Record, $datum);
254
255
        // call template function
256
        static::onBeforeRecordValidated($Record, $datum);
257
258
        // try to save record
259
        try {
260
            // call template function
261
            static::onBeforeRecordSaved($Record, $datum);
262
263
            $Record->save();
264
            return (!$Record::fieldExists('Class') || get_class($Record) == $Record->Class) ? $Record : $Record->changeClass();
265
            
266
            // call template function
267
            static::onRecordSaved($Record, $datum);
0 ignored issues
show
Unused Code introduced by
static::onRecordSaved($Record, $datum) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
268
        } catch (Exception $e) {
269
            $failed[] = [
0 ignored issues
show
Comprehensibility Best Practice introduced by
$failed was never initialized. Although not strictly required by PHP, it is generally a good practice to add $failed = array(); before regardless.
Loading history...
270
                'record' => $Record->data,
271
                'validationErrors' => $Record->validationErrors,
272
            ];
273
        }
274
    }
275
276
    public static function handleMultiSaveRequest()
277
    {
278
        $className = static::$recordClass;
279
    
280
        $PrimaryKey = $className::getPrimaryKey();
0 ignored issues
show
Unused Code introduced by
The assignment to $PrimaryKey is dead and can be removed.
Loading history...
281
            
282
        static::prepareResponseModeJSON(['POST','PUT']);
283
        
284
        if ($className::fieldExists(key($_REQUEST['data']))) {
285
            $_REQUEST['data'] = [$_REQUEST['data']];
286
        }
287
288
        if (empty($_REQUEST['data']) || !is_array($_REQUEST['data'])) {
289
            return static::respond('error', [
290
                'success' => false,
291
                'failed' => [
292
                    'errors'	=>	'Save expects "data" field as array of records.',
293
                ],
294
            ]);
295
        }
296
        
297
        $results = [];
298
        $failed = [];
299
300
        foreach ($_REQUEST['data'] as $datum) {
301
            try {
302
                $results[] = static::processDatumSave($datum);
303
            } catch (Exception $e) {
304
                $failed[] = [
305
                    'record' => $datum,
306
                    'errors' => $e->getMessage(),
307
                ];
308
                continue;
309
            }
310
        }
311
        
312
        
313
        return static::respond(static::getTemplateName($className::$pluralNoun).'Saved', [
314
            'success' => count($results) || !count($failed),
315
            'data' => $results,
316
            'failed' => $failed,
317
        ]);
318
    }
319
    
320
    
321
    public static function handleMultiDestroyRequest()
322
    {
323
        $className = static::$recordClass;
324
325
        $PrimaryKey = $className::getPrimaryKey();
326
327
        static::prepareResponseModeJSON(['POST','PUT','DELETE']);
328
        
329
        if ($className::fieldExists(key($_REQUEST['data']))) {
330
            $_REQUEST['data'] = [$_REQUEST['data']];
331
        }
332
333
        if (empty($_REQUEST['data']) || !is_array($_REQUEST['data'])) {
334
            return static::respond('error', [
335
                'success' => false,
336
                'failed' => [
337
                    'errors'	=>	'Save expects "data" field as array of records.',
338
                ],
339
            ]);
340
        }
341
342
343
        $results = [];
344
        $failed = [];
345
        
346
        foreach ($_REQUEST['data'] as $datum) {
347
            // get record
348
            if (is_numeric($datum)) {
349
                $recordID = $datum;
350
            } elseif (!empty($datum[$PrimaryKey]) && is_numeric($datum[$PrimaryKey])) {
351
                $recordID = $datum[$PrimaryKey];
352
            } else {
353
                $failed[] = [
354
                    'record' => $datum,
355
                    'errors' => $PrimaryKey.' missing',
356
                ];
357
                continue;
358
            }
359
360
            if (!$Record = $className::getByField($PrimaryKey, $recordID)) {
361
                $failed[] = [
362
                    'record' => $datum,
363
                    'errors' => $PrimaryKey.' not found',
364
                ];
365
                continue;
366
            }
367
            
368
            // check write access
369
            if (!static::checkWriteAccess($Record)) {
370
                $failed[] = [
371
                    'record' => $datum,
372
                    'errors' => 'Write access denied',
373
                ];
374
                continue;
375
            }
376
        
377
            // destroy record
378
            if ($Record->destroy()) {
379
                $results[] = $Record;
380
            }
381
        }
382
        
383
        return static::respond(static::getTemplateName($className::$pluralNoun).'Destroyed', [
384
            'success' => count($results) || !count($failed),
385
            'data' => $results,
386
            'failed' => $failed,
387
        ]);
388
    }
389
390
391
    public static function handleCreateRequest(ActiveRecord $Record = null)
392
    {
393
        // save static class
394
        static::$calledClass = get_called_class();
395
396
        if (!$Record) {
397
            $className = static::$recordClass;
398
            $Record = new $className::$defaultClass();
399
        }
400
        
401
        // call template function
402
        static::onRecordCreated($Record, $_REQUEST);
403
404
        return static::handleEditRequest($Record);
405
    }
406
407
    public static function handleEditRequest(ActiveRecord $Record)
408
    {
409
        $className = static::$recordClass;
410
411
        if (!static::checkWriteAccess($Record)) {
412
            return static::throwUnauthorizedError();
413
        }
414
415
        if (in_array($_SERVER['REQUEST_METHOD'], ['POST','PUT'])) {
416
            if (static::$responseMode == 'json') {
417
                $_REQUEST = JSON::getRequestData();
418
                if (is_array($_REQUEST['data'])) {
419
                    $_REQUEST = $_REQUEST['data'];
420
                }
421
            }
422
            $_REQUEST = $_REQUEST ? $_REQUEST : $_POST;
423
        
424
            // apply delta
425
            static::applyRecordDelta($Record, $_REQUEST);
426
            
427
            // call template function
428
            static::onBeforeRecordValidated($Record, $_REQUEST);
429
430
            // validate
431
            if ($Record->validate()) {
432
                // call template function
433
                static::onBeforeRecordSaved($Record, $_REQUEST);
434
                
435
                try {
436
                    // save session
437
                    $Record->save();
438
                } catch (Exception $e) {
439
                    return static::respond('Error', [
440
                        'success' => false,
441
                        'failed' => [
442
                            'errors'	=>	$e->getMessage(),
443
                        ],
444
                    ]);
445
                }
446
                
447
                // call template function
448
                static::onRecordSaved($Record, $_REQUEST);
449
        
450
                // fire created response
451
                $responseID = static::getTemplateName($className::$singularNoun).'Saved';
452
                $responseData = [
453
                    'success' => true,
454
                    'data' => $Record,
455
                ];
456
                return static::respond($responseID, $responseData);
457
            }
458
            
459
            // fall through back to form if validation failed
460
        }
461
        
462
        $responseID = static::getTemplateName($className::$singularNoun).'Edit';
463
        $responseData = [
464
            'success' => false,
465
            'data' => $Record,
466
        ];
467
    
468
        return static::respond($responseID, $responseData);
469
    }
470
471
472
    public static function handleDeleteRequest(ActiveRecord $Record)
473
    {
474
        $className = static::$recordClass;
475
476
        if (!static::checkWriteAccess($Record)) {
477
            return static::throwUnauthorizedError();
478
        }
479
    
480
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
481
            $data = $Record->data;
482
            $Record->destroy();
483
                    
484
            // call cleanup function after delete
485
            static::onRecordDeleted($Record, $data);
486
            
487
            // fire created response
488
            return static::respond(static::getTemplateName($className::$singularNoun).'Deleted', [
489
                'success' => true,
490
                'data' => $Record,
491
            ]);
492
        }
493
    
494
        return static::respond('confirm', [
495
            'question' => 'Are you sure you want to delete this '.$className::$singularNoun.'?',
496
            'data' => $Record,
497
        ]);
498
    }
499
    
500
    
501
    public static function respond($responseID, $responseData = [], $responseMode = false)
502
    {
503
        // default to static property
504
        if (!$responseMode) {
505
            $responseMode = static::$responseMode;
506
        }
507
    
508
        return parent::respond($responseID, $responseData, $responseMode);
509
    }
510
    
511
    // access control template functions
512
    public static function checkBrowseAccess($arguments)
513
    {
514
        return true;
515
    }
516
517
    public static function checkReadAccess(ActiveRecord $Record)
518
    {
519
        return true;
520
    }
521
    
522
    public static function checkWriteAccess(ActiveRecord $Record)
523
    {
524
        return true;
525
    }
526
    
527
    public static function checkAPIAccess()
528
    {
529
        return true;
530
    }
531
    
532
    public static function throwUnauthorizedError()
533
    {
534
        return static::respond('Unauthorized', [
535
            'success' => false,
536
            'failed' => [
537
                'errors'	=>	'Login required.',
538
            ],
539
        ]);
540
    }
541
542
    public static function throwAPIUnAuthorizedError()
543
    {
544
        return static::respond('Unauthorized', [
545
            'success' => false,
546
            'failed' => [
547
                'errors'	=>	'API access required.',
548
            ],
549
        ]);
550
    }
551
552
    public static function throwNotFoundError()
553
    {
554
        return static::respond('error', [
555
            'success' => false,
556
            'failed' => [
557
                'errors'	=>	'Record not found.',
558
            ],
559
        ]);
560
    }
561
    
562
    public static function onRecordRequestNotHandled(ActiveRecord $Record, $action)
563
    {
564
        return static::respond('error', [
565
            'success' => false,
566
            'failed' => [
567
                'errors'	=>	'Malformed request.',
568
            ],
569
        ]);
570
    }
571
    
572
573
574
    public static function getTemplateName($noun)
575
    {
576
        return preg_replace_callback('/\s+([a-zA-Z])/', function ($matches) {
577
            return strtoupper($matches[1]);
578
        }, $noun);
579
    }
580
    
581
    public static function applyRecordDelta(ActiveRecord $Record, $data)
582
    {
583
        if (is_array(static::$editableFields)) {
584
            $Record->setFields(array_intersect_key($data, array_flip(static::$editableFields)));
585
        } else {
586
            $Record->setFields($data);
587
        }
588
    }
589
    
590
    // event template functions
591
    protected static function onRecordCreated(ActiveRecord $Record, $data)
592
    {
593
    }
594
    protected static function onBeforeRecordValidated(ActiveRecord $Record, $data)
595
    {
596
    }
597
    protected static function onBeforeRecordSaved(ActiveRecord $Record, $data)
598
    {
599
    }
600
    protected static function onRecordDeleted(ActiveRecord $Record, $data)
601
    {
602
    }
603
    protected static function onRecordSaved(ActiveRecord $Record, $data)
604
    {
605
    }
606
    
607
    protected static function throwRecordNotFoundError()
608
    {
609
        return static::throwNotFoundError();
610
    }
611
}
612