Test Failed
Push — master ( 6f63e6...b60734 )
by Julien
04:24
created

Rest::arrayCustomOrder()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 3
eloc 5
nc 3
nop 2
dl 0
loc 9
ccs 0
cts 5
cp 0
crap 12
rs 10
c 3
b 0
f 0
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
    
32
    /**
33
     * @throws Exception
34
     */
35
    public function indexAction(?int $id = null)
36
    {
37
        $this->restForwarding($id);
38
    }
39
    
40
    /**
41
     * Rest bootstrap forwarding
42
     * @throws Exception
43
     */
44
    protected function restForwarding(?int $id = null): void
45
    {
46
        $id ??= $this->getParam('id');
47
        if ($this->request->isPost() || $this->request->isPut() || $this->request->isPatch()) {
48
            $this->dispatcher->forward(['action' => 'save']);
49
        }
50
        elseif ($this->request->isDelete()) {
51
            $this->dispatcher->forward(['action' => 'delete']);
52
        }
53
        elseif ($this->request->isGet()) {
54
            if (is_null($id)) {
55
                $this->dispatcher->forward(['action' => 'getList']);
56
            }
57
            else {
58
                $this->dispatcher->forward(['action' => 'get']);
59
            }
60
        }
61
    }
62
    
63
    /**
64
     * Retrieving a single record
65
     * Alias of method getAction()
66
     *
67
     * @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...
68
     * @return bool|ResponseInterface
69
     * @deprecated Should use getAction() method instead
70
     */
71
    public function getSingleAction($id = null)
72
    {
73
        return $this->getAction($id);
74
    }
75
    
76
    /**
77
     * Retrieving a single record
78
     *
79
     * @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...
80
     *
81
     * @return bool|ResponseInterface
82
     */
83
    public function getAction($id = null)
84
    {
85
        $modelName = $this->getModelClassName();
86
        $single = $this->getSingle($id, $modelName, null);
87
        
88
        $ret = [];
89
        $ret['single'] = $single ? $this->expose($single) : false;
90
        $ret['model'] = $modelName;
91
        $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

91
        $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...
92
        $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

92
        $this->view->/** @scrutinizer ignore-call */ 
93
                     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...
93
        
94
        if (!$single) {
95
            $this->response->setStatusCode(404, 'Not Found');
96
            return false;
97
        }
98
        
99
        return $this->setRestResponse((bool)$single);
100
    }
101
    
102
    /**
103
     * Retrieving a record list
104
     * Alias of method getListAction()
105
     *
106
     * @return ResponseInterface
107
     * @throws \Exception
108
     * @deprecated Should use getListAction() method instead
109
     */
110
    public function getAllAction()
111
    {
112
        return $this->getListAction();
113
    }
114
    
115
    /**
116
     * Retrieving a record list
117
     *
118
     * @return ResponseInterface
119
     * @throws \Exception
120
     */
121
    public function getListAction()
122
    {
123
        $model = $this->getModelClassName();
124
        
125
        $find = $this->getFind() ?: [];
126
        $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...
127
        
128
        $resultset = $model::findWith($with, $find);
129
        $list = $this->listExpose($resultset);
130
        
131
        $ret = [];
132
        $ret['list'] = $list;
133
        $ret['listCount'] = count($list);
134
        $ret['totalCount'] = $model::find($this->getFindCount($find)); // @todo fix count to work with rollup when joins
135
        $ret['totalCount'] = is_int($ret['totalCount']) ? $ret['totalCount'] : count($ret['totalCount']);
136
        $ret['limit'] = $find['limit'] ?? null;
137
        $ret['offset'] = $find['offset'] ?? null;
138
        
139
        if ($this->isDebugEnabled()) {
140
            $ret['find'] = $find;
141
            $ret['with'] = $with;
142
        }
143
        
144
        $this->view->setVars($ret);
145
        
146
        return $this->setRestResponse((bool)$resultset);
147
    }
148
    
149
    /**
150
     * Exporting a record list into a CSV stream
151
     *
152
     * @return ResponseInterface|null
153
     * @throws \Exception
154
     */
155
    public function exportAction()
156
    {
157
        $model = $this->getModelClassName();
158
        $find = $this->getFind();
159
        $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...
160
        $list = $this->exportExpose($with);
161
        $this->flatternArrayForCsv($list);
162
        $this->formatColumnText($list);
163
        return $this->download($list);
164
    }
165
    
166
    /**
167
     * Download a JSON / CSV / XLSX
168
     * @param $list
169
     * @param $fileName
170
     * @param $contentType
171
     * @param $params
172
     * @return ResponseInterface|void
173
     * @throws \League\Csv\CannotInsertRecord
174
     * @throws \League\Csv\InvalidArgument
175
     * @throws \Zemit\Exception
176
     */
177
    public function download($list = [], $fileName = null, $contentType = null, $params = null)
178
    {
179
        $params ??= $this->getParams();
180
        $contentType ??= $this->getContentType();
181
        $fileName ??= ucfirst(Slug::generate(basename(str_replace('\\', '/', $this->getModelClassName())))) . ' List (' . date('Y-m-d') . ')';
182
        
183
        if ($contentType === 'json') {
184
//            $this->response->setJsonContent($list);
185
            $this->response->setContent(json_encode($list, JSON_PRETTY_PRINT, 2048));
186
            $this->response->setContentType('application/json');
187
            $this->response->setHeader('Content-disposition', 'attachment; filename="' . addslashes($fileName) . '.json"');
188
            return $this->response->send();
189
        }
190
        
191
        // CSV
192
        if ($contentType === 'csv') {
193
        
194
            // Get CSV custom request parameters
195
            $mode = $params['mode'] ?? null;
196
            $delimiter = $params['delimiter'] ?? null;
197
            $newline = $params['newline'] ?? null;
198
            $escape = $params['escape'] ?? null;
199
            $outputBOM = $params['outputBOM'] ?? null;
200
            $skipIncludeBOM = $params['skipIncludeBOM'] ?? null;
201
202
//            $csv = Writer::createFromFileObject(new \SplTempFileObject());
203
            $csv = Writer::createFromStream(fopen('php://memory', 'r+'));
204
        
205
            // CSV - MS Excel on MacOS
206
            if ($mode === 'mac') {
207
                $csv->setOutputBOM(Writer::BOM_UTF16_LE); // utf-16
208
                $csv->setDelimiter("\t"); // tabs separated
209
                $csv->setNewline("\r\n"); // new lines
210
                CharsetConverter::addTo($csv, 'UTF-8', 'UTF-16');
211
            }
212
            
213
            // CSV - MS Excel on Windows
214
            else {
215
                $csv->setOutputBOM(Writer::BOM_UTF8); // utf-8
216
                $csv->setDelimiter(','); // comma separated
217
                $csv->setNewline("\n"); // new line windows
218
                CharsetConverter::addTo($csv, 'UTF-8', 'UTF-8');
219
            }
220
            
221
            // Apply forced params from request
222
            if (isset($outputBOM)) {
223
                $csv->setOutputBOM($outputBOM);
224
            }
225
            if (isset($delimiter)) {
226
                $csv->setDelimiter($delimiter);
227
            }
228
            if (isset($newline)) {
229
                $csv->setNewline($newline);
230
            }
231
            if (isset($escape)) {
232
                $csv->setEscape($escape);
233
            }
234
            if ($skipIncludeBOM) {
235
                $csv->skipInputBOM();
236
            }
237
            else {
238
                $csv->includeInputBOM();
239
            }
240
            
241
            // CSV
242
            if (isset($list[0])) {
243
                $csv->insertOne(array_keys($list[0]));
244
                $csv->insertAll($list);
245
            }
246
            $csv->output($fileName . '.csv');
247
            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...
248
        }
249
        
250
        // XLSX
251
        if ($contentType === 'xlsx') {
252
            $xlsxArray = [];
253
            if (isset($list[0])) {
254
                $xlsxArray [] = array_keys($list[0]);
255
            }
256
            foreach ($list as $array) {
257
                $xlsxArray [] = array_values($array);
258
            }
259
            $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...
260
            $xlsx->downloadAs($fileName . '.xlsx');
261
            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...
262
        }
263
        
264
        // Something went wrong
265
        throw new \Exception('Failed to export `' . $this->getModelClassName() . '` using content-type `' . $contentType . '`', 400);
266
    }
267
    
268
    /**
269
     * Expose a single model
270
     */
271
    public function expose(ModelInterface $item, ?array $expose = null): array
272
    {
273
        $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...
274
        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

274
        return $item->/** @scrutinizer ignore-call */ expose($expose);
Loading history...
275
    }
276
    
277
    /**
278
     * Expose a list of model
279
     */
280
    public function listExpose(iterable $items, ?array $listExpose = null): array
281
    {
282
        $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...
283
        $ret = [];
284
        
285
        foreach ($items as $item) {
286
            $ret [] = $this->expose($item, $listExpose);
287
        }
288
        
289
        return $ret;
290
    }
291
    
292
    /**
293
     * Expose a list of model
294
     */
295
    public function exportExpose(iterable $items, $exportExpose = null): array
296
    {
297
        $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...
298
        return $this->listExpose($items, $exportExpose);
299
    }
300
    
301
    /**
302
     * @param array|null $array
303
     *
304
     * @return array|null
305
     */
306
    public function flatternArrayForCsv(?array &$list = null)
307
    {
308
        
309
        foreach ($list as $listKey => $listValue) {
310
            foreach ($listValue as $column => $value) {
311
                if (is_array($value) || is_object($value)) {
312
                    $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

312
                    $value = $this->concatListFieldElementForCsv(/** @scrutinizer ignore-type */ $value, ' ');
Loading history...
313
                    $list[$listKey][$column] = $this->arrayFlatten($value, $column);
314
                    if (is_array($list[$listKey][$column])) {
315
                        foreach ($list[$listKey][$column] as $childKey => $childValue) {
316
                            $list[$listKey][$childKey] = $childValue;
317
                            unset($list[$listKey][$column]);
318
                        }
319
                    }
320
                }
321
            }
322
        }
323
    }
324
    
325
    public function concatListFieldElementForCsv(array $list = [], ?string $seperator = ' '): array
326
    {
327
        foreach ($list as $valueKey => $element) {
328
            if (is_array($element) || is_object($element)) {
329
                $lastKey = array_key_last($list);
330
                if ($valueKey === $lastKey) {
331
                    continue;
332
                }
333
                foreach ($element as $elKey => $elValue) {
334
                    $list[$lastKey][$elKey] .= $seperator . $elValue;
335
                    if ($lastKey != $valueKey) {
336
                        unset($list[$valueKey]);
337
                    }
338
                }
339
            }
340
        }
341
        
342
        return $list;
343
    }
344
    
345
    public function arrayFlatten(?array $array, ?string $alias = null): array
346
    {
347
        $ret = [];
348
        foreach ($array as $key => $value) {
349
            if (is_array($value)) {
350
                $ret = array_merge($ret, $this->arrayFlatten($value, $alias));
351
            }
352
            else {
353
                $ret[$alias . '.' . $key] = $value;
354
            }
355
        }
356
        return $ret;
357
    }
358
    
359
    /**
360
     * @param array|null $listValue
361
     *
362
     * @return array|null
363
     */
364
    public function mergeColumns(?array $listValue)
365
    {
366
        $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...
367
        if (!$columnToMergeList || empty($columnToMergeList)) {
0 ignored issues
show
introduced by
$columnToMergeList is of type null, thus it always evaluated to false.
Loading history...
368
            return $listValue;
369
        }
370
        
371
        $columnList = [];
372
        foreach ($columnToMergeList as $columnToMerge) {
373
            foreach ($columnToMerge['columns'] as $column) {
374
                if (isset($listValue[$column])) {
375
                    $columnList[$columnToMerge['name']][] = $listValue[$column];
376
                    unset($listValue[$column]);
377
                }
378
            }
379
            $listValue[$columnToMerge['name']] = implode(' ', $columnList[$columnToMerge['name']] ?? []);
380
        }
381
        
382
        return $listValue;
383
    }
384
    
385
    /**
386
     * @param array|null $list
387
     *
388
     * @return array|null
389
     */
390
    public function formatColumnText(?array &$list)
391
    {
392
        foreach ($list as $listKey => $listValue) {
393
            
394
            $mergeColumArray = $this->mergeColumns($listValue);
395
            if (!empty($mergeColumArray)) {
396
                $list[$listKey] = $mergeColumArray;
397
            }
398
            
399
            $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...
400
            if ($formatArray) {
401
                $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

401
                $columNameList = array_keys(/** @scrutinizer ignore-type */ $formatArray);
Loading history...
402
                foreach ($formatArray as $formatKey => $formatValue) {
0 ignored issues
show
Bug introduced by
The expression $formatArray of type void is not traversable.
Loading history...
403
                    if (isset($formatValue['text'])) {
404
                        $list[$listKey][$formatKey] = $formatValue['text'];
405
                    }
406
                    
407
                    if (isset($formatValue['rename'])) {
408
                        
409
                        $list[$listKey][$formatValue['rename']] = $formatValue['text'] ?? ($list[$listKey][$formatKey] ?? null);
410
                        if ($formatValue['rename'] !== $formatKey) {
411
                            foreach ($columNameList as $columnKey => $columnValue) {
412
                                
413
                                if ($formatKey === $columnValue) {
414
                                    $columNameList[$columnKey] = $formatValue['rename'];
415
                                }
416
                            }
417
                            
418
                            unset($list[$listKey][$formatKey]);
419
                        }
420
                    }
421
                }
422
                
423
                if (isset($formatArray['reorderColumns']) && $formatArray['reorderColumns']) {
424
                    $list[$listKey] = $this->arrayCustomOrder($list[$listKey], $columNameList);
425
                }
426
            }
427
        }
428
        
429
        return $list;
430
    }
431
    
432
    public function arrayCustomOrder(array $arrayToOrder, array $orderList): array
433
    {
434
        $ordered = [];
435
        foreach ($orderList as $key) {
436
            if (array_key_exists($key, $arrayToOrder)) {
437
                $ordered[$key] = $arrayToOrder[$key];
438
            }
439
        }
440
        return $ordered;
441
    }
442
    
443
    /**
444
     * Count a record list
445
     * @TODO add total count / deleted count / active count
446
     *
447
     * @return ResponseInterface
448
     */
449
    public function countAction()
450
    {
451
        $model = $this->getModelClassName();
452
        
453
        /** @var \Zemit\Mvc\Model $entity */
454
        $entity = new $model();
455
        
456
        $ret = [];
457
        $ret['totalCount'] = $model::count($this->getFindCount($this->getFind()));
458
        $ret['totalCount'] = is_int($ret['totalCount']) ? $ret['totalCount'] : count($ret['totalCount']);
459
        $ret['model'] = get_class($entity);
460
        $ret['source'] = $entity->getSource();
461
        $this->view->setVars($ret);
462
        
463
        return $this->setRestResponse();
464
    }
465
    
466
    /**
467
     * Prepare a new model for the frontend
468
     *
469
     * @return ResponseInterface
470
     */
471
    public function newAction()
472
    {
473
        $model = $this->getModelClassName();
474
        
475
        /** @var \Zemit\Mvc\Model $entity */
476
        $entity = new $model();
477
        $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...
478
        
479
        $this->view->model = get_class($entity);
480
        $this->view->source = $entity->getSource();
481
        $this->view->single = $this->expose($entity);
482
        
483
        return $this->setRestResponse();
484
    }
485
    
486
    /**
487
     * Prepare a new model for the frontend
488
     *
489
     * @return ResponseInterface
490
     */
491
    public function validateAction($id = null)
492
    {
493
        $model = $this->getModelClassName();
494
        
495
        /** @var \Zemit\Mvc\Model $entity */
496
        $entity = $this->getSingle($id);
497
        $new = !$entity;
0 ignored issues
show
introduced by
$entity is of type Zemit\Mvc\Model, thus it always evaluated to true.
Loading history...
498
        
499
        if ($new) {
0 ignored issues
show
introduced by
The condition $new is always false.
Loading history...
500
            $entity = new $model();
501
        }
502
        
503
        $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...
504
        
505
        /**
506
         * Event to run
507
         * @see https://docs.phalcon.io/4.0/en/db-models-events
508
         */
509
        $events = [
510
            'beforeCreate' => null,
511
            'beforeUpdate' => null,
512
            'beforeSave' => null,
513
            'beforeValidationOnCreate' => null,
514
            'beforeValidationOnUpdate' => null,
515
            'beforeValidation' => null,
516
            'prepareSave' => null,
517
            'validation' => null,
518
            'afterValidationOnCreate' => null,
519
            'afterValidationOnUpdate' => null,
520
            'afterValidation' => null,
521
        ];
522
        
523
        // run events, as it would normally
524
        foreach ($events as $event => $state) {
525
            $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...
526
            
527
            // skip depending wether it's a create or update
528
            if (strpos($event, $new ? 'Update' : 'Create') !== false) {
529
                continue;
530
            }
531
            
532
            // fire the event, allowing to fail or skip
533
            $events[$event] = $entity->fireEventCancel($event);
534
            if ($events[$event] === false) {
535
                // event failed
536
                break;
537
            }
538
            
539
            // event was skipped, just for consistencies purpose
540
            if ($this->skipped) {
541
                continue;
542
            }
543
        }
544
    
545
        $ret = [];
546
        $ret['model'] = get_class($entity);
547
        $ret['source'] = $entity->getSource();
548
        $ret['single'] = $this->expose($entity);
549
        $ret['messages'] = $entity->getMessages();
550
        $ret['events'] = $events;
551
        $ret['validated'] = empty($this->view->messages);
552
        $this->view->setVars($ret);
553
        
554
        return $this->setRestResponse($ret['validated']);
555
    }
556
    
557
    /**
558
     * Saving a record (create & update)
559
     *
560
     * @param null|int $id
561
     *
562
     * @return ResponseInterface
563
     */
564
    public function saveAction($id = null)
565
    {
566
        $ret = $this->save($id);
567
        $this->view->setVars($ret);
568
        
569
        return $this->setRestResponse(true); // @todo set value based on save single or list
570
    }
571
    
572
    /**
573
     * Deleting a record
574
     *
575
     * @param null|int $id
576
     *
577
     * @return bool|ResponseInterface
578
     */
579
    public function deleteAction($id = null)
580
    {
581
        $entity = $this->getSingle($id);
582
        
583
        $ret = [];
584
        $ret['deleted'] = $entity && $entity->delete();
585
        $ret['single'] = $entity ? $this->expose($entity) : false;
586
        $ret['messages'] = $entity ? $entity->getMessages() : false;
587
        $this->view->setVars($ret);
588
        
589
        if (!$entity) {
590
            $this->response->setStatusCode(404, 'Not Found');
591
            return false;
592
        }
593
        
594
        return $this->setRestResponse($ret['deleted']);
595
    }
596
    
597
    /**
598
     * Restoring record
599
     *
600
     * @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...
601
     *
602
     * @return bool|ResponseInterface
603
     */
604
    public function restoreAction($id = null)
605
    {
606
        $entity = $this->getSingle($id);
607
        
608
        $ret = [];
609
        $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

609
        $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...
610
        $ret['single'] = $entity ? $this->expose($entity) : false;
611
        $ret['messages'] = $entity ? $entity->getMessages() : false;
612
        $this->view->setVars($ret);
613
        
614
        if (!$entity) {
615
            $this->response->setStatusCode(404, 'Not Found');
616
            return false;
617
        }
618
        
619
        return $this->setRestResponse($ret['restored']);
620
    }
621
    
622
    /**
623
     * Re-ordering a position
624
     *
625
     * @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...
626
     * @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...
627
     *
628
     * @return bool|ResponseInterface
629
     */
630
    public function reorderAction($id = null, $position = null)
631
    {
632
        $entity = $this->getSingle($id);
633
        $position = $this->getParam('position', 'int', $position);
634
        
635
        $ret = [];
636
        $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

636
        $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...
637
        $ret['single'] = $entity ? $this->expose($entity) : false;
638
        $ret['messages'] = $entity ? $entity->getMessages() : false;
639
        $this->view->setVars($ret);
640
        
641
        if (!$entity) {
642
            $this->response->setStatusCode(404, 'Not Found');
643
            return false;
644
        }
645
        
646
        return $this->setRestResponse($ret['reordered']);
647
    }
648
    
649
    /**
650
     * Sending an error as an http response
651
     *
652
     * @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...
653
     * @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...
654
     *
655
     * @return ResponseInterface
656
     */
657
    public function setRestErrorResponse($code = 400, $status = 'Bad Request', $response = null)
658
    {
659
        return $this->setRestResponse($response, $code, $status);
660
    }
661
    
662
    /**
663
     * Sending rest response as an http response
664
     *
665
     * @param mixed $response
666
     * @param ?int $code
667
     * @param ?string $status
668
     * @param int $jsonOptions
669
     * @param int $depth
670
     *
671
     * @return ResponseInterface
672
     */
673
    public function setRestResponse($response = null, int $code = null, string $status = null, int $jsonOptions = 0, int $depth = 512): ResponseInterface
674
    {
675
        $debug = $this->isDebugEnabled();
676
        
677
        // keep forced status code or set our own
678
        $statusCode = $this->response->getStatusCode();
679
        $reasonPhrase = $this->response->getReasonPhrase();
680
        $code ??= (int)$statusCode ?: 200;
681
        $status ??= $reasonPhrase ?: StatusCode::getMessage($code);
682
        
683
        $view = $this->view->getParamsToView();
684
        $hash = hash('sha512', json_encode($view));
685
    
686
        // set response status code
687
        $this->response->setStatusCode($code, $code . ' ' . $status);
688
        
689
        // @todo handle this correctly
690
        // @todo private vs public cache type
691
        $cache = $this->getCache();
692
        if (!empty($cache['lifetime'])) {
693
            if ($this->response->getStatusCode() === 200) {
694
                $this->response->setCache($cache['lifetime']);
695
                $this->response->setEtag($hash);
696
            }
697
        }
698
        else {
699
            $this->response->setCache(0);
700
            $this->response->setHeader('Cache-Control', 'no-cache, max-age=0');
701
        }
702
        
703
        $ret = [];
704
        $ret['api'] = [];
705
        $ret['api']['version'] = ['0.1']; // @todo
706
        $ret['timestamp'] = date('c');
707
        $ret['hash'] = $hash;
708
        $ret['status'] = $status;
709
        $ret['code'] = $code;
710
        $ret['response'] = $response;
711
        $ret['view'] = $view;
712
        
713
        if ($debug) {
714
            $ret['api']['php'] = phpversion();
715
            $ret['api']['phalcon'] = Version::get();
716
            $ret['api']['zemit'] = $this->config->path('core.version');
717
            $ret['api']['core'] = $this->config->path('core.name');
718
            $ret['api']['app'] = $this->config->path('app.version');
719
            $ret['api']['name'] = $this->config->path('app.name');
720
    
721
            $ret['identity'] = $this->identity ? $this->identity->getIdentity() : null;
722
            $ret['profiler'] = $this->profiler ? $this->profiler->toArray() : null;
723
            $ret['request'] = $this->request ? $this->request->toArray() : null;
724
            $ret['dispatcher'] = $this->dispatcher ? $this->dispatcher->toArray() : null;
725
            $ret['router'] = $this->router ? $this->router->toArray() : null;
726
            $ret['memory'] = Utils::getMemoryUsage();
727
        }
728
        
729
        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

729
        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...
730
    }
731
    
732
    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

732
    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...
733
    {
734
        // @todo use eventsManager from service provider instead
735
        $this->eventsManager->enablePriorities(true);
736
        
737
        // @todo see if we can implement receiving an array of responses globally: V2
738
        // $this->eventsManager->collectResponses(true);
739
        
740
        // retrieve events based on the config roles and features
741
        $permissions = $this->config->get('permissions')->toArray() ?? [];
742
        $featureList = $permissions['features'] ?? [];
743
        $roleList = $permissions['roles'] ?? [];
744
        
745
        foreach ($roleList as $role => $rolePermission) {
746
            
747
            if (isset($rolePermission['features'])) {
748
                foreach ($rolePermission['features'] as $feature) {
749
                    $rolePermission = array_merge_recursive($rolePermission, $featureList[$feature] ?? []);
750
                    // @todo remove duplicates
751
                }
752
            }
753
            
754
            $behaviorsContext = $rolePermission['behaviors'] ?? [];
755
            foreach ($behaviorsContext as $className => $behaviors) {
756
                if (is_int($className) || get_class($this) === $className) {
757
                    $this->attachBehaviors($behaviors, 'rest');
758
                }
759
                if ($this->getModelClassName() === $className) {
760
                    $this->attachBehaviors($behaviors, 'model');
761
                }
762
            }
763
        }
764
    }
765
    
766
    /**
767
     * Attach a new behavior
768
     * @todo review behavior type
769
     */
770
    public function attachBehavior(string $behavior, string $eventType = 'rest'): void
771
    {
772
        $event = new $behavior();
773
        
774
        // inject DI
775
        if ($event instanceof Injectable || method_exists($event, 'setDI')) {
776
            $event->setDI($this->getDI());
777
        }
778
        
779
        // attach behavior
780
        $this->eventsManager->attach($event->eventType ?? $eventType, $event, $event->priority ?? Manager::DEFAULT_PRIORITY);
0 ignored issues
show
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

780
        $this->eventsManager->/** @scrutinizer ignore-call */ 
781
                              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...
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...
781
    }
782
    
783
    /**
784
     * Attach new behaviors
785
     */
786
    public function attachBehaviors(array $behaviors = [], string $eventType = 'rest'): void
787
    {
788
        foreach ($behaviors as $behavior) {
789
            $this->attachBehavior($behavior, $eventType);
790
        }
791
    }
792
    
793
    /**
794
     * Handle rest response automagically
795
     */
796
    public function afterExecuteRoute(Dispatcher $dispatcher): void
797
    {
798
        $response = $dispatcher->getReturnedValue();
799
        
800
        // Avoid breaking default phalcon behaviour
801
        if ($response instanceof Response) {
802
            return;
803
        }
804
        
805
        // Merge response into view variables
806
        if (is_array($response)) {
807
            $this->view->setVars($response, true);
808
        }
809
        
810
        // Return our Rest normalized response
811
        $dispatcher->setReturnedValue($this->setRestResponse(is_array($response) ? null : $response));
812
    }
813
    
814
    public function isDebugEnabled(): bool
815
    {
816
        return $this->config->path('app.debug') ?? false;
817
    }
818
}
819