Test Failed
Push — master ( 90bc11...d3d0f9 )
by Julien
13:14
created

Rest::countAction()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 2
eloc 9
c 3
b 0
f 0
nc 2
nop 0
dl 0
loc 15
ccs 0
cts 10
cp 0
crap 6
rs 9.9666
1
<?php
2
3
/**
4
 * This file is part of the Zemit Framework.
5
 *
6
 * (c) Zemit Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Zemit\Mvc\Controller;
13
14
use League\Csv\CharsetConverter;
15
use League\Csv\Writer;
16
use Phalcon\Events\Manager;
17
use Phalcon\Exception;
18
use Phalcon\Http\Response;
19
use Phalcon\Http\ResponseInterface;
20
use Phalcon\Mvc\Dispatcher;
21
use Phalcon\Mvc\ModelInterface;
22
use Phalcon\Version;
23
use Zemit\Di\Injectable;
24
use Zemit\Http\StatusCode;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Zemit\Mvc\Controller\StatusCode. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
25
use Zemit\Utils;
26
use Zemit\Utils\Slug;
27
28
class Rest extends \Zemit\Mvc\Controller
29
{
30
    use Model;
0 ignored issues
show
Bug introduced by
The trait Zemit\Mvc\Controller\Model requires the property $loader which is not provided by Zemit\Mvc\Controller\Rest.
Loading history...
31
    use Rest\Fractal;
32
    
33
    /**
34
     * @throws Exception
35
     */
36
    public function indexAction(?int $id = null)
37
    {
38
        $this->restForwarding($id);
39
    }
40
    
41
    /**
42
     * Rest bootstrap forwarding
43
     * @throws Exception
44
     */
45
    protected function restForwarding(?int $id = null): void
46
    {
47
        $id ??= $this->getParam('id');
48
        if ($this->request->isPost() || $this->request->isPut() || $this->request->isPatch()) {
49
            $this->dispatcher->forward(['action' => 'save']);
50
        }
51
        elseif ($this->request->isDelete()) {
52
            $this->dispatcher->forward(['action' => 'delete']);
53
        }
54
        elseif ($this->request->isGet()) {
55
            if (is_null($id)) {
56
                $this->dispatcher->forward(['action' => 'getList']);
57
            }
58
            else {
59
                $this->dispatcher->forward(['action' => 'get']);
60
            }
61
        }
62
    }
63
    
64
    /**
65
     * Retrieving a single record
66
     * Alias of method getAction()
67
     *
68
     * @param null $id
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $id is correct as it would always require null to be passed?
Loading history...
69
     * @return bool|ResponseInterface
70
     * @deprecated Should use getAction() method instead
71
     */
72
    public function getSingleAction($id = null)
73
    {
74
        return $this->getAction($id);
75
    }
76
    
77
    /**
78
     * Retrieving a single record
79
     *
80
     * @param null $id
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $id is correct as it would always require null to be passed?
Loading history...
81
     *
82
     * @return bool|ResponseInterface
83
     */
84
    public function getAction($id = null)
85
    {
86
        $modelName = $this->getModelClassName();
87
        $single = $this->getSingle($id, $modelName, null);
88
        
89
        $ret = [];
90
        $ret['single'] = $single ? $this->expose($single) : false;
91
        $ret['model'] = $modelName;
92
        $ret['source'] = $single ? $single->getSource() : false;
0 ignored issues
show
Bug introduced by
The method getSource() does not exist on Phalcon\Mvc\Model\Resultset. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

92
        $ret['source'] = $single ? $single->/** @scrutinizer ignore-call */ getSource() : false;

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
93
        $this->view->setVars($ret);
0 ignored issues
show
Bug introduced by
The method setVars() does not exist on Phalcon\Mvc\ViewInterface. Did you maybe mean setVar()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

93
        $this->view->/** @scrutinizer ignore-call */ 
94
                     setVars($ret);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
94
        
95
        if (!$single) {
96
            $this->response->setStatusCode(404, 'Not Found');
97
            return false;
98
        }
99
        
100
        return $this->setRestResponse((bool)$single);
101
    }
102
    
103
    /**
104
     * Retrieving a record list
105
     * Alias of method getListAction()
106
     *
107
     * @return ResponseInterface
108
     * @throws \Exception
109
     * @deprecated Should use getListAction() method instead
110
     */
111
    public function getAllAction()
112
    {
113
        return $this->getListAction();
114
    }
115
    
116
    /**
117
     * Retrieving a record list
118
     *
119
     * @return ResponseInterface
120
     * @throws \Exception
121
     */
122
    public function getListAction()
123
    {
124
        $model = $this->getModelClassName();
125
        
126
        $find = $this->getFind() ?: [];
127
        $with = $this->getListWith() ?: [];
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getListWith() targeting Zemit\Mvc\Controller\Rest::getListWith() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
128
        
129
        $resultset = $model::findWith($with, $find);
130
        $list = $this->listExpose($resultset);
131
        
132
        $ret = [];
133
        $ret['list'] = $list;
134
        $ret['listCount'] = count($list);
135
        $ret['totalCount'] = $model::find($this->getFindCount($find)); // @todo fix count to work with rollup when joins
136
        $ret['totalCount'] = is_int($ret['totalCount']) ? $ret['totalCount'] : count($ret['totalCount']);
137
        $ret['limit'] = $find['limit'] ?? null;
138
        $ret['offset'] = $find['offset'] ?? null;
139
        
140
        if ($this->isDebugEnabled()) {
141
            $ret['find'] = $find;
142
            $ret['with'] = $with;
143
        }
144
        
145
        $this->view->setVars($ret);
146
        
147
        return $this->setRestResponse((bool)$resultset);
148
    }
149
    
150
    /**
151
     * Exporting a record list into a CSV stream
152
     *
153
     * @return ResponseInterface|null
154
     * @throws \Exception
155
     */
156
    public function exportAction()
157
    {
158
        $model = $this->getModelClassName();
159
        $find = $this->getFind();
160
        $with = $model::with($this->getExportWith() ?: [], $find ?: []);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getExportWith() targeting Zemit\Mvc\Controller\Rest::getExportWith() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
161
        $list = $this->exportExpose($with);
162
        $this->flatternArrayForCsv($list);
163
        $this->formatColumnText($list);
164
        return $this->download($list);
165
    }
166
    
167
    /**
168
     * Download a JSON / CSV / XLSX
169
     *
170
     * @TODO optimize this using stream and avoid storage large dataset into variables
171
     *
172
     * @param $list
173
     * @param $fileName
174
     * @param $contentType
175
     * @param $params
176
     * @return ResponseInterface|void
177
     * @throws \League\Csv\CannotInsertRecord
178
     * @throws \League\Csv\InvalidArgument
179
     * @throws \Zemit\Exception
180
     */
181
    public function download($list = [], $fileName = null, $contentType = null, $params = null)
182
    {
183
        $params ??= $this->getParams();
184
        $contentType ??= $this->getContentType();
185
        $fileName ??= ucfirst(Slug::generate(basename(str_replace('\\', '/', $this->getModelClassName())))) . ' List (' . date('Y-m-d') . ')';
186
        
187
        if ($contentType === 'json') {
188
//            $this->response->setJsonContent($list);
189
            $this->response->setContent(json_encode($list, JSON_PRETTY_PRINT, 2048));
190
            $this->response->setContentType('application/json');
191
            $this->response->setHeader('Content-disposition', 'attachment; filename="' . addslashes($fileName) . '.json"');
192
            return $this->response->send();
193
        }
194
        
195
        $listColumns = [];
196
        if ($contentType === 'csv' || $contentType === 'xlsx') {
197
            foreach ($list as $row) {
198
                foreach (array_keys($row) as $key) {
199
                    $listColumns[$key] = true;
200
                }
201
            }
202
        }
203
        $listColumns = array_keys($listColumns);
204
        
205
        // CSV
206
        if ($contentType === 'csv') {
207
            
208
            // Get CSV custom request parameters
209
            $mode = $params['mode'] ?? null;
210
            $delimiter = $params['delimiter'] ?? null;
211
            $newline = $params['newline'] ?? null;
212
            $escape = $params['escape'] ?? null;
213
            $outputBOM = $params['outputBOM'] ?? null;
214
            $skipIncludeBOM = $params['skipIncludeBOM'] ?? null;
215
216
//            $csv = Writer::createFromFileObject(new \SplTempFileObject());
217
            $csv = Writer::createFromStream(fopen('php://memory', 'r+'));
218
            
219
            // CSV - MS Excel on MacOS
220
            if ($mode === 'mac') {
221
                $csv->setOutputBOM(Writer::BOM_UTF16_LE); // utf-16
222
                $csv->setDelimiter("\t"); // tabs separated
223
                $csv->setNewline("\r\n"); // new lines
224
                CharsetConverter::addTo($csv, 'UTF-8', 'UTF-16');
225
            }
226
            
227
            // CSV - MS Excel on Windows
228
            else {
229
                $csv->setOutputBOM(Writer::BOM_UTF8); // utf-8
230
                $csv->setDelimiter(','); // comma separated
231
                $csv->setNewline("\n"); // new line windows
232
                CharsetConverter::addTo($csv, 'UTF-8', 'UTF-8');
233
            }
234
            
235
            // Apply forced params from request
236
            if (isset($outputBOM)) {
237
                $csv->setOutputBOM($outputBOM);
238
            }
239
            if (isset($delimiter)) {
240
                $csv->setDelimiter($delimiter);
241
            }
242
            if (isset($newline)) {
243
                $csv->setNewline($newline);
244
            }
245
            if (isset($escape)) {
246
                $csv->setEscape($escape);
247
            }
248
            if ($skipIncludeBOM) {
249
                $csv->skipInputBOM();
250
            }
251
            else {
252
                $csv->includeInputBOM();
253
            }
254
            
255
            // Headers
256
            $csv->insertOne($listColumns);
257
            
258
            foreach ($list as $row) {
259
                $outputRow = [];
260
                foreach ($listColumns as $column) {
261
                    $outputRow[$column] = $row[$column] ?? '';
262
                }
263
                $csv->insertOne($outputRow);
264
            }
265
            
266
            // CSV
267
            $csv->output($fileName . '.csv');
268
            die;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
269
        }
270
        
271
        // XLSX
272
        if ($contentType === 'xlsx') {
273
            $xlsxArray = [];
274
            $xlsxArray []= $listColumns;
275
            
276
            foreach ($list as $row) {
277
                $outputRow = [];
278
                foreach ($listColumns as $column) {
279
                    $outputRow[$column] = $row[$column] ?? '';
280
                }
281
                $xlsxArray [] = array_values($outputRow);
282
            }
283
            
284
            $xlsx = \SimpleXLSXGen::fromArray($xlsxArray);
0 ignored issues
show
Bug introduced by
The type SimpleXLSXGen was not found. Maybe you did not declare it correctly or list all dependencies?

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:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
285
            $xlsx->downloadAs($fileName . '.xlsx');
286
            die;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
287
        }
288
        
289
        // Something went wrong
290
        throw new \Exception('Failed to export `' . $this->getModelClassName() . '` using content-type `' . $contentType . '`', 400);
291
    }
292
    
293
    /**
294
     * Expose a single model
295
     */
296
    public function expose(ModelInterface $item, ?array $expose = null): array
297
    {
298
        $expose ??= $this->getExpose();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getExpose() targeting Zemit\Mvc\Controller\Rest::getExpose() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
299
        return $item->expose($expose);
0 ignored issues
show
Bug introduced by
The method expose() does not exist on Phalcon\Mvc\ModelInterface. It seems like you code against a sub-type of Phalcon\Mvc\ModelInterface such as Phalcon\Mvc\Model or Zemit\Models\Setting or Zemit\Models\Category or Zemit\Models\Audit or Zemit\Models\UserGroup or Zemit\Models\User or Zemit\Models\Field or Zemit\Models\Page or Zemit\Models\Log or Zemit\Models\File or Zemit\Models\Role or Zemit\Models\GroupRole or Zemit\Models\Template or Zemit\Models\AuditDetail or Zemit\Models\UserType or Zemit\Models\Post or Zemit\Models\PostCategory or Zemit\Models\Session or Zemit\Models\TranslateField or Zemit\Models\GroupType or Zemit\Models\Translate or Zemit\Models\Email or Zemit\Models\Data or Zemit\Models\Group or Zemit\Models\Lang or Zemit\Models\EmailFile or Zemit\Models\TranslateTable or Zemit\Models\SiteLang or Zemit\Models\UserRole or Zemit\Models\Flag or Zemit\Models\Menu or Zemit\Models\Site or Zemit\Models\Type or Zemit\Models\Channel or Zemit\Models\Meta. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

299
        return $item->/** @scrutinizer ignore-call */ expose($expose);
Loading history...
300
    }
301
    
302
    /**
303
     * Expose a list of model
304
     */
305
    public function listExpose(iterable $items, ?array $listExpose = null): array
306
    {
307
        $listExpose ??= $this->getListExpose();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getListExpose() targeting Zemit\Mvc\Controller\Rest::getListExpose() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
308
        $ret = [];
309
        
310
        foreach ($items as $item) {
311
            $ret [] = $this->expose($item, $listExpose);
312
        }
313
        
314
        return $ret;
315
    }
316
    
317
    /**
318
     * Expose a list of model
319
     */
320
    public function exportExpose(iterable $items, $exportExpose = null): array
321
    {
322
        $exportExpose ??= $this->getExportExpose();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getExportExpose() targeting Zemit\Mvc\Controller\Rest::getExportExpose() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
323
        return $this->listExpose($items, $exportExpose);
324
    }
325
    
326
    /**
327
     * @param array|null $array
328
     *
329
     * @return array|null
330
     */
331
    public function flatternArrayForCsv(?array &$list = null)
332
    {
333
        
334
        foreach ($list as $listKey => $listValue) {
335
            foreach ($listValue as $column => $value) {
336
                if (is_array($value) || is_object($value)) {
337
                    $value = $this->concatListFieldElementForCsv($value, ' ');
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type object; however, parameter $list of Zemit\Mvc\Controller\Res...istFieldElementForCsv() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

337
                    $value = $this->concatListFieldElementForCsv(/** @scrutinizer ignore-type */ $value, ' ');
Loading history...
338
                    $list[$listKey][$column] = $this->arrayFlatten($value, $column);
339
                    if (is_array($list[$listKey][$column])) {
340
                        foreach ($list[$listKey][$column] as $childKey => $childValue) {
341
                            $list[$listKey][$childKey] = $childValue;
342
                            unset($list[$listKey][$column]);
343
                        }
344
                    }
345
                }
346
            }
347
        }
348
    }
349
    
350
    public function concatListFieldElementForCsv(array $list = [], ?string $seperator = ' '): array
351
    {
352
        foreach ($list as $valueKey => $element) {
353
            if (is_array($element) || is_object($element)) {
354
                $lastKey = array_key_last($list);
355
                if ($valueKey === $lastKey) {
356
                    continue;
357
                }
358
                foreach ($element as $elKey => $elValue) {
359
                    $list[$lastKey][$elKey] .= $seperator . $elValue;
360
                    if ($lastKey != $valueKey) {
361
                        unset($list[$valueKey]);
362
                    }
363
                }
364
            }
365
        }
366
        
367
        return $list;
368
    }
369
    
370
    public function arrayFlatten(?array $array, ?string $alias = null): array
371
    {
372
        $ret = [];
373
        foreach ($array as $key => $value) {
374
            if (is_array($value)) {
375
                $ret = array_merge($ret, $this->arrayFlatten($value, $alias));
376
            }
377
            else {
378
                $ret[$alias . '.' . $key] = $value;
379
            }
380
        }
381
        return $ret;
382
    }
383
    
384
    /**
385
     * @param array|null $listValue
386
     *
387
     * @return array|null
388
     */
389
    public function mergeColumns(?array $listValue)
390
    {
391
        $columnToMergeList = $this->getExportMergeColum();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $columnToMergeList is correct as $this->getExportMergeColum() targeting Zemit\Mvc\Controller\Rest::getExportMergeColum() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
392
        if (!$columnToMergeList || empty($columnToMergeList)) {
0 ignored issues
show
introduced by
$columnToMergeList is of type null, thus it always evaluated to false.
Loading history...
393
            return $listValue;
394
        }
395
        
396
        $columnList = [];
397
        foreach ($columnToMergeList as $columnToMerge) {
398
            foreach ($columnToMerge['columns'] as $column) {
399
                if (isset($listValue[$column])) {
400
                    $columnList[$columnToMerge['name']][] = $listValue[$column];
401
                    unset($listValue[$column]);
402
                }
403
            }
404
            $listValue[$columnToMerge['name']] = implode(' ', $columnList[$columnToMerge['name']] ?? []);
405
        }
406
        
407
        return $listValue;
408
    }
409
    
410
    /**
411
     * @param array|null $list
412
     *
413
     * @return array|null
414
     */
415
    public function formatColumnText(?array &$list)
416
    {
417
        foreach ($list as $listKey => $listValue) {
418
            
419
            $mergeColumArray = $this->mergeColumns($listValue);
420
            if (!empty($mergeColumArray)) {
421
                $list[$listKey] = $mergeColumArray;
422
            }
423
            
424
            $formatArray = $this->getExportFormatFieldText($listValue);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $formatArray is correct as $this->getExportFormatFieldText($listValue) targeting Zemit\Mvc\Controller\Res...ExportFormatFieldText() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
425
            if ($formatArray) {
426
                $columNameList = array_keys($formatArray);
0 ignored issues
show
Bug introduced by
$formatArray of type void is incompatible with the type array expected by parameter $array of array_keys(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

426
                $columNameList = array_keys(/** @scrutinizer ignore-type */ $formatArray);
Loading history...
427
                foreach ($formatArray as $formatKey => $formatValue) {
0 ignored issues
show
Bug introduced by
The expression $formatArray of type void is not traversable.
Loading history...
428
                    if (isset($formatValue['text'])) {
429
                        $list[$listKey][$formatKey] = $formatValue['text'];
430
                    }
431
                    
432
                    if (isset($formatValue['rename'])) {
433
                        
434
                        $list[$listKey][$formatValue['rename']] = $formatValue['text'] ?? ($list[$listKey][$formatKey] ?? null);
435
                        if ($formatValue['rename'] !== $formatKey) {
436
                            foreach ($columNameList as $columnKey => $columnValue) {
437
                                
438
                                if ($formatKey === $columnValue) {
439
                                    $columNameList[$columnKey] = $formatValue['rename'];
440
                                }
441
                            }
442
                            
443
                            unset($list[$listKey][$formatKey]);
444
                        }
445
                    }
446
                }
447
                
448
                if (isset($formatArray['reorderColumns']) && $formatArray['reorderColumns']) {
449
                    $list[$listKey] = $this->arrayCustomOrder($list[$listKey], $columNameList);
450
                }
451
            }
452
        }
453
        
454
        return $list;
455
    }
456
    
457
    public function arrayCustomOrder(array $arrayToOrder, array $orderList): array
458
    {
459
        $ordered = [];
460
        foreach ($orderList as $key) {
461
            if (array_key_exists($key, $arrayToOrder)) {
462
                $ordered[$key] = $arrayToOrder[$key];
463
            }
464
        }
465
        return $ordered;
466
    }
467
    
468
    /**
469
     * Count a record list
470
     * @TODO add total count / deleted count / active count
471
     *
472
     * @return ResponseInterface
473
     */
474
    public function countAction()
475
    {
476
        $model = $this->getModelClassName();
477
        
478
        /** @var \Zemit\Mvc\Model $entity */
479
        $entity = new $model();
480
        
481
        $ret = [];
482
        $ret['totalCount'] = $model::count($this->getFindCount($this->getFind()));
483
        $ret['totalCount'] = is_int($ret['totalCount']) ? $ret['totalCount'] : count($ret['totalCount']);
484
        $ret['model'] = get_class($entity);
485
        $ret['source'] = $entity->getSource();
486
        $this->view->setVars($ret);
487
        
488
        return $this->setRestResponse();
489
    }
490
    
491
    /**
492
     * Prepare a new model for the frontend
493
     *
494
     * @return ResponseInterface
495
     */
496
    public function newAction()
497
    {
498
        $model = $this->getModelClassName();
499
        
500
        /** @var \Zemit\Mvc\Model $entity */
501
        $entity = new $model();
502
        $entity->assign($this->getParams(), $this->getWhiteList(), $this->getColumnMap());
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getColumnMap() targeting Zemit\Mvc\Controller\Rest::getColumnMap() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
Are you sure the usage of $this->getWhiteList() targeting Zemit\Mvc\Controller\Rest::getWhiteList() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
503
        
504
        $this->view->model = get_class($entity);
505
        $this->view->source = $entity->getSource();
506
        $this->view->single = $this->expose($entity);
507
        
508
        return $this->setRestResponse();
509
    }
510
    
511
    /**
512
     * Prepare a new model for the frontend
513
     *
514
     * @return ResponseInterface
515
     */
516
    public function validateAction($id = null)
517
    {
518
        $model = $this->getModelClassName();
519
        
520
        /** @var \Zemit\Mvc\Model $entity */
521
        $entity = $this->getSingle($id);
522
        $new = !$entity;
0 ignored issues
show
introduced by
$entity is of type Zemit\Mvc\Model, thus it always evaluated to true.
Loading history...
523
        
524
        if ($new) {
0 ignored issues
show
introduced by
The condition $new is always false.
Loading history...
525
            $entity = new $model();
526
        }
527
        
528
        $entity->assign($this->getParams(), $this->getWhiteList(), $this->getColumnMap());
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getColumnMap() targeting Zemit\Mvc\Controller\Rest::getColumnMap() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
Are you sure the usage of $this->getWhiteList() targeting Zemit\Mvc\Controller\Rest::getWhiteList() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
529
        
530
        /**
531
         * Event to run
532
         * @see https://docs.phalcon.io/4.0/en/db-models-events
533
         */
534
        $events = [
535
            'beforeCreate' => null,
536
            'beforeUpdate' => null,
537
            'beforeSave' => null,
538
            'beforeValidationOnCreate' => null,
539
            'beforeValidationOnUpdate' => null,
540
            'beforeValidation' => null,
541
            'prepareSave' => null,
542
            'validation' => null,
543
            'afterValidationOnCreate' => null,
544
            'afterValidationOnUpdate' => null,
545
            'afterValidation' => null,
546
        ];
547
        
548
        // run events, as it would normally
549
        foreach ($events as $event => $state) {
550
            $this->skipped = false;
0 ignored issues
show
Bug Best Practice introduced by
The property skipped does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
551
            
552
            // skip depending wether it's a create or update
553
            if (strpos($event, $new ? 'Update' : 'Create') !== false) {
554
                continue;
555
            }
556
            
557
            // fire the event, allowing to fail or skip
558
            $events[$event] = $entity->fireEventCancel($event);
559
            if ($events[$event] === false) {
560
                // event failed
561
                break;
562
            }
563
            
564
            // event was skipped, just for consistencies purpose
565
            if ($this->skipped) {
566
                continue;
567
            }
568
        }
569
        
570
        $ret = [];
571
        $ret['model'] = get_class($entity);
572
        $ret['source'] = $entity->getSource();
573
        $ret['single'] = $this->expose($entity);
574
        $ret['messages'] = $entity->getMessages();
575
        $ret['events'] = $events;
576
        $ret['validated'] = empty($this->view->messages);
577
        $this->view->setVars($ret);
578
        
579
        return $this->setRestResponse($ret['validated']);
580
    }
581
    
582
    /**
583
     * Saving a record (create & update)
584
     */
585
    public function saveAction(?int $id = null): ?ResponseInterface
586
    {
587
        $ret = $this->save($id);
588
        $this->view->setVars($ret);
589
        $saved = $this->saveResultHasKey($ret, 'saved');
590
        $messages = $this->saveResultHasKey($ret, 'messages');
591
        
592
        if (!$saved) {
593
            if (!$messages) {
594
                $this->response->setStatusCode(422, 'Unprocessable Entity');
595
            }
596
            else {
597
                $this->response->setStatusCode(400, 'Bad Request');
598
            }
599
        }
600
        
601
        return $this->setRestResponse($saved);
602
    }
603
    
604
    /**
605
     * Return true if the record or the records where saved
606
     * Return false if one record wasn't saved
607
     * Return null if nothing was saved
608
     */
609
    public function saveResultHasKey(array $array, string $key): bool
610
    {
611
        $ret = $array[$key] ?? null;
612
        
613
        if (isset($array[0])) {
614
            foreach ($array as $k => $r) {
615
                if (isset($r[$key])) {
616
                    if ($r[$key]) {
617
                        $ret = true;
618
                    }
619
                    else {
620
                        $ret = false;
621
                        break;
622
                    }
623
                }
624
            }
625
        }
626
        
627
        return (bool)$ret;
628
    }
629
    
630
    /**
631
     * Deleting a record
632
     */
633
    public function deleteAction(?int $id = null): ?ResponseInterface
634
    {
635
        $entity = $this->getSingle($id);
636
        
637
        $ret = [];
638
        $ret['deleted'] = $entity && $entity->delete();
639
        $ret['single'] = $entity ? $this->expose($entity) : false;
640
        $ret['messages'] = $entity ? $entity->getMessages() : false;
641
        $this->view->setVars($ret);
642
        
643
        if (!$entity) {
644
            $this->response->setStatusCode(404, 'Not Found');
645
        }
646
        
647
        return $this->setRestResponse($ret['deleted']);
648
    }
649
    
650
    /**
651
     * Restoring record
652
     *
653
     * @param null $id
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $id is correct as it would always require null to be passed?
Loading history...
654
     *
655
     * @return bool|ResponseInterface
656
     */
657
    public function restoreAction($id = null)
658
    {
659
        $entity = $this->getSingle($id);
660
        
661
        $ret = [];
662
        $ret['restored'] = $entity && $entity->restore();
0 ignored issues
show
Bug introduced by
The method restore() does not exist on Phalcon\Mvc\Model\Resultset. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

662
        $ret['restored'] = $entity && $entity->/** @scrutinizer ignore-call */ restore();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
663
        $ret['single'] = $entity ? $this->expose($entity) : false;
664
        $ret['messages'] = $entity ? $entity->getMessages() : false;
665
        $this->view->setVars($ret);
666
        
667
        if (!$entity) {
668
            $this->response->setStatusCode(404, 'Not Found');
669
            return false;
670
        }
671
        
672
        return $this->setRestResponse($ret['restored']);
673
    }
674
    
675
    /**
676
     * Re-ordering a position
677
     *
678
     * @param null $id
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $id is correct as it would always require null to be passed?
Loading history...
679
     * @param null $position
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $position is correct as it would always require null to be passed?
Loading history...
680
     *
681
     * @return bool|ResponseInterface
682
     */
683
    public function reorderAction($id = null, $position = null)
684
    {
685
        $entity = $this->getSingle($id);
686
        $position = $this->getParam('position', 'int', $position);
687
        
688
        $ret = [];
689
        $ret['reordered'] = $entity ? $entity->reorder($position) : false;
0 ignored issues
show
Bug introduced by
The method reorder() does not exist on Phalcon\Mvc\Model\Resultset. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

689
        $ret['reordered'] = $entity ? $entity->/** @scrutinizer ignore-call */ reorder($position) : false;

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
690
        $ret['single'] = $entity ? $this->expose($entity) : false;
691
        $ret['messages'] = $entity ? $entity->getMessages() : false;
692
        $this->view->setVars($ret);
693
        
694
        if (!$entity) {
695
            $this->response->setStatusCode(404, 'Not Found');
696
            return false;
697
        }
698
        
699
        return $this->setRestResponse($ret['reordered']);
700
    }
701
    
702
    /**
703
     * Sending an error as an http response
704
     *
705
     * @param null $error
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $error is correct as it would always require null to be passed?
Loading history...
706
     * @param null $response
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $response is correct as it would always require null to be passed?
Loading history...
707
     *
708
     * @return ResponseInterface
709
     */
710
    public function setRestErrorResponse($code = 400, $status = 'Bad Request', $response = null)
711
    {
712
        return $this->setRestResponse($response, $code, $status);
713
    }
714
    
715
    /**
716
     * Sending rest response as an http response
717
     *
718
     * @param mixed $response
719
     * @param ?int $code
720
     * @param ?string $status
721
     * @param int $jsonOptions
722
     * @param int $depth
723
     *
724
     * @return ResponseInterface
725
     */
726
    public function setRestResponse($response = null, int $code = null, string $status = null, int $jsonOptions = 0, int $depth = 512): ResponseInterface
727
    {
728
        $debug = $this->isDebugEnabled();
729
        
730
        // keep forced status code or set our own
731
        $statusCode = $this->response->getStatusCode();
732
        $reasonPhrase = $this->response->getReasonPhrase();
733
        $code ??= (int)$statusCode ?: 200;
734
        $status ??= $reasonPhrase ?: StatusCode::getMessage($code);
735
        
736
        $view = $this->view->getParamsToView();
737
        $hash = hash('sha512', json_encode($view));
738
        
739
        // set response status code
740
        $this->response->setStatusCode($code, $code . ' ' . $status);
741
        
742
        // @todo handle this correctly
743
        // @todo private vs public cache type
744
        $cache = $this->getCache();
745
        if (!empty($cache['lifetime'])) {
746
            if ($this->response->getStatusCode() === 200) {
747
                $this->response->setCache($cache['lifetime']);
748
                $this->response->setEtag($hash);
749
            }
750
        }
751
        else {
752
            $this->response->setCache(0);
753
            $this->response->setHeader('Cache-Control', 'no-cache, max-age=0');
754
        }
755
        
756
        $ret = [];
757
        $ret['api'] = [];
758
        $ret['api']['version'] = ['0.1']; // @todo
759
        $ret['timestamp'] = date('c');
760
        $ret['hash'] = $hash;
761
        $ret['status'] = $status;
762
        $ret['code'] = $code;
763
        $ret['response'] = $response;
764
        $ret['view'] = $view;
765
        
766
        if ($debug) {
767
            $ret['api']['php'] = phpversion();
768
            $ret['api']['phalcon'] = Version::get();
769
            $ret['api']['zemit'] = $this->config->path('core.version');
770
            $ret['api']['core'] = $this->config->path('core.name');
771
            $ret['api']['app'] = $this->config->path('app.version');
772
            $ret['api']['name'] = $this->config->path('app.name');
773
            
774
            $ret['identity'] = $this->identity ? $this->identity->getIdentity() : null;
775
            $ret['profiler'] = $this->profiler ? $this->profiler->toArray() : null;
776
            $ret['request'] = $this->request ? $this->request->toArray() : null;
777
            $ret['dispatcher'] = $this->dispatcher ? $this->dispatcher->toArray() : null;
778
            $ret['router'] = $this->router ? $this->router->toArray() : null;
779
            $ret['memory'] = Utils::getMemoryUsage();
780
        }
781
        
782
        return $this->response->setJsonContent($ret, $jsonOptions, $depth);
0 ignored issues
show
Unused Code introduced by
The call to Phalcon\Http\ResponseInterface::setJsonContent() has too many arguments starting with $jsonOptions. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

782
        return $this->response->/** @scrutinizer ignore-call */ setJsonContent($ret, $jsonOptions, $depth);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
783
    }
784
    
785
    public function beforeExecuteRoute(Dispatcher $dispatcher): void
0 ignored issues
show
Unused Code introduced by
The parameter $dispatcher is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

785
    public function beforeExecuteRoute(/** @scrutinizer ignore-unused */ Dispatcher $dispatcher): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
786
    {
787
        // @todo use eventsManager from service provider instead
788
        $this->eventsManager->enablePriorities(true);
789
        
790
        // @todo see if we can implement receiving an array of responses globally: V2
791
        // $this->eventsManager->collectResponses(true);
792
        
793
        // retrieve events based on the config roles and features
794
        $permissions = $this->config->get('permissions')->toArray() ?? [];
795
        $featureList = $permissions['features'] ?? [];
796
        $roleList = $permissions['roles'] ?? [];
797
        
798
        foreach ($roleList as $role => $rolePermission) {
799
            // do not attach other roles behaviors
800
            if (!$this->identity->hasRole([$role])) {
801
                continue;
802
            }
803
            
804
            if (isset($rolePermission['features'])) {
805
                foreach ($rolePermission['features'] as $feature) {
806
                    $rolePermission = array_merge_recursive($rolePermission, $featureList[$feature] ?? []);
807
                    // @todo remove duplicates
808
                }
809
            }
810
            
811
            $behaviorsContext = $rolePermission['behaviors'] ?? [];
812
            foreach ($behaviorsContext as $className => $behaviors) {
813
                if (is_int($className) || get_class($this) === $className) {
814
                    $this->attachBehaviors($behaviors, 'rest');
815
                }
816
                if ($this->getModelClassName() === $className) {
817
                    $this->attachBehaviors($behaviors, 'model');
818
                }
819
            }
820
        }
821
    }
822
    
823
    /**
824
     * Attach a new behavior
825
     * @todo review behavior type
826
     */
827
    public function attachBehavior(string $behavior, string $eventType = 'rest'): void
828
    {
829
        $event = new $behavior();
830
        
831
        // inject DI
832
        if ($event instanceof Injectable || method_exists($event, 'setDI')) {
833
            $event->setDI($this->getDI());
834
        }
835
        
836
        // attach behavior
837
        $this->eventsManager->attach($event->eventType ?? $eventType, $event, $event->priority ?? Manager::DEFAULT_PRIORITY);
0 ignored issues
show
Bug Best Practice introduced by
The property priority does not exist on Zemit\Di\Injectable. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property eventType does not exist on Zemit\Di\Injectable. Since you implemented __get, consider adding a @property annotation.
Loading history...
Unused Code introduced by
The call to Phalcon\Events\ManagerInterface::attach() has too many arguments starting with $event->priority ?? Phal...nager::DEFAULT_PRIORITY. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

837
        $this->eventsManager->/** @scrutinizer ignore-call */ 
838
                              attach($event->eventType ?? $eventType, $event, $event->priority ?? Manager::DEFAULT_PRIORITY);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
838
    }
839
    
840
    /**
841
     * Attach new behaviors
842
     */
843
    public function attachBehaviors(array $behaviors = [], string $eventType = 'rest'): void
844
    {
845
        foreach ($behaviors as $behavior) {
846
            $this->attachBehavior($behavior, $eventType);
847
        }
848
    }
849
    
850
    /**
851
     * Handle rest response automagically
852
     */
853
    public function afterExecuteRoute(Dispatcher $dispatcher): void
854
    {
855
        $response = $dispatcher->getReturnedValue();
856
        
857
        // Avoid breaking default phalcon behaviour
858
        if ($response instanceof Response) {
859
            return;
860
        }
861
        
862
        // Merge response into view variables
863
        if (is_array($response)) {
864
            $this->view->setVars($response, true);
865
        }
866
        
867
        // Return our Rest normalized response
868
        $dispatcher->setReturnedValue($this->setRestResponse(is_array($response) ? null : $response));
869
    }
870
    
871
    public function isDebugEnabled(): bool
872
    {
873
        return $this->config->path('app.debug') ?? false;
874
    }
875
}
876