Passed
Push — master ( a67e20...527622 )
by Henry
02:03
created

RecordsRequestHandler::getRecordByHandle()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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