Test Failed
Push — master ( ae5494...dff97a )
by Julien
12:01
created

Rest::concatListFieldElementForCsv()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 7
eloc 10
c 3
b 1
f 0
nc 4
nop 2
dl 0
loc 18
ccs 0
cts 10
cp 0
crap 56
rs 8.8333
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;
31
    use Rest\Fractal;
0 ignored issues
show
Bug introduced by
The type Zemit\Mvc\Controller\Rest\Fractal 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...
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
     * @param $list
170
     * @param $fileName
171
     * @param $contentType
172
     * @param $params
173
     * @return ResponseInterface|void
174
     * @throws \League\Csv\CannotInsertRecord
175
     * @throws \League\Csv\InvalidArgument
176
     * @throws \Zemit\Exception
177
     */
178
    public function download($list = [], $fileName = null, $contentType = null, $params = null)
179
    {
180
        $params ??= $this->getParams();
181
        $contentType ??= $this->getContentType();
182
        $fileName ??= ucfirst(Slug::generate(basename(str_replace('\\', '/', $this->getModelClassName())))) . ' List (' . date('Y-m-d') . ')';
183
        
184
        if ($contentType === 'json') {
185
//            $this->response->setJsonContent($list);
186
            $this->response->setContent(json_encode($list, JSON_PRETTY_PRINT, 2048));
187
            $this->response->setContentType('application/json');
188
            $this->response->setHeader('Content-disposition', 'attachment; filename="' . addslashes($fileName) . '.json"');
189
            return $this->response->send();
190
        }
191
        
192
        // CSV
193
        if ($contentType === 'csv') {
194
            
195
            // Get CSV custom request parameters
196
            $mode = $params['mode'] ?? null;
197
            $delimiter = $params['delimiter'] ?? null;
198
            $newline = $params['newline'] ?? null;
199
            $escape = $params['escape'] ?? null;
200
            $outputBOM = $params['outputBOM'] ?? null;
201
            $skipIncludeBOM = $params['skipIncludeBOM'] ?? null;
202
203
//            $csv = Writer::createFromFileObject(new \SplTempFileObject());
204
            $csv = Writer::createFromStream(fopen('php://memory', 'r+'));
205
            
206
            // CSV - MS Excel on MacOS
207
            if ($mode === 'mac') {
208
                $csv->setOutputBOM(Writer::BOM_UTF16_LE); // utf-16
209
                $csv->setDelimiter("\t"); // tabs separated
210
                $csv->setNewline("\r\n"); // new lines
211
                CharsetConverter::addTo($csv, 'UTF-8', 'UTF-16');
212
            }
213
            
214
            // CSV - MS Excel on Windows
215
            else {
216
                $csv->setOutputBOM(Writer::BOM_UTF8); // utf-8
217
                $csv->setDelimiter(','); // comma separated
218
                $csv->setNewline("\n"); // new line windows
219
                CharsetConverter::addTo($csv, 'UTF-8', 'UTF-8');
220
            }
221
            
222
            // Apply forced params from request
223
            if (isset($outputBOM)) {
224
                $csv->setOutputBOM($outputBOM);
225
            }
226
            if (isset($delimiter)) {
227
                $csv->setDelimiter($delimiter);
228
            }
229
            if (isset($newline)) {
230
                $csv->setNewline($newline);
231
            }
232
            if (isset($escape)) {
233
                $csv->setEscape($escape);
234
            }
235
            if ($skipIncludeBOM) {
236
                $csv->skipInputBOM();
237
            }
238
            else {
239
                $csv->includeInputBOM();
240
            }
241
            
242
            // CSV
243
            if (isset($list[0])) {
244
                $csv->insertOne(array_keys($list[0]));
245
                $csv->insertAll($list);
246
            }
247
            $csv->output($fileName . '.csv');
248
            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...
249
        }
250
        
251
        // XLSX
252
        if ($contentType === 'xlsx') {
253
            $xlsxArray = [];
254
            if (isset($list[0])) {
255
                $xlsxArray [] = array_keys($list[0]);
256
            }
257
            foreach ($list as $array) {
258
                $xlsxArray [] = array_values($array);
259
            }
260
            $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...
261
            $xlsx->downloadAs($fileName . '.xlsx');
262
            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...
263
        }
264
        
265
        // Something went wrong
266
        throw new \Exception('Failed to export `' . $this->getModelClassName() . '` using content-type `' . $contentType . '`', 400);
267
    }
268
    
269
    /**
270
     * Expose a single model
271
     */
272
    public function expose(ModelInterface $item, ?array $expose = null): array
273
    {
274
        $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...
275
        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

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

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

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

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
568
            if (empty($ret['messages'])) {
569
                $this->response->setStatusCode(422, 'Unprocessable Entity');
570
            }
571
            else {
572
                $this->response->setStatusCode(400, 'Bad Request');
573
            }
574
        }
575
        
576
        return $this->setRestResponse($saved);
577
    }
578
    
579
    /**
580
     * Return true if the record or the records where saved
581
     * Return false if one record wasn't saved
582
     * Return null if nothing was saved
583
     */
584
    public function isSaved(array $array): ?bool
585
    {
586
        $saved = $ret['saved'] ?? null;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $ret seems to never exist and therefore isset should always be false.
Loading history...
587
        
588
        if (isset($ret[0])) {
589
            foreach ($ret as $k => $r) {
590
                if (isset($r['saved'])) {
591
                    if ($r['saved']) {
592
                        $saved = true;
593
                    }
594
                    else {
595
                        $saved = false;
596
                        break;
597
                    }
598
                }
599
            }
600
        }
601
        
602
        return $saved;
603
    }
604
    
605
    /**
606
     * Deleting a record
607
     */
608
    public function deleteAction(?int $id = null): ?ResponseInterface
609
    {
610
        $entity = $this->getSingle($id);
611
        
612
        $ret = [];
613
        $ret['deleted'] = $entity && $entity->delete();
614
        $ret['single'] = $entity ? $this->expose($entity) : false;
615
        $ret['messages'] = $entity ? $entity->getMessages() : false;
616
        $this->view->setVars($ret);
617
        
618
        if (!$entity) {
619
            $this->response->setStatusCode(404, 'Not Found');
620
        }
621
        
622
        return $this->setRestResponse($ret['deleted']);
623
    }
624
    
625
    /**
626
     * Restoring record
627
     *
628
     * @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...
629
     *
630
     * @return bool|ResponseInterface
631
     */
632
    public function restoreAction($id = null)
633
    {
634
        $entity = $this->getSingle($id);
635
        
636
        $ret = [];
637
        $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

637
        $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...
638
        $ret['single'] = $entity ? $this->expose($entity) : false;
639
        $ret['messages'] = $entity ? $entity->getMessages() : false;
640
        $this->view->setVars($ret);
641
        
642
        if (!$entity) {
643
            $this->response->setStatusCode(404, 'Not Found');
644
            return false;
645
        }
646
        
647
        return $this->setRestResponse($ret['restored']);
648
    }
649
    
650
    /**
651
     * Re-ordering a position
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
     * @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...
655
     *
656
     * @return bool|ResponseInterface
657
     */
658
    public function reorderAction($id = null, $position = null)
659
    {
660
        $entity = $this->getSingle($id);
661
        $position = $this->getParam('position', 'int', $position);
662
        
663
        $ret = [];
664
        $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

664
        $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...
665
        $ret['single'] = $entity ? $this->expose($entity) : false;
666
        $ret['messages'] = $entity ? $entity->getMessages() : false;
667
        $this->view->setVars($ret);
668
        
669
        if (!$entity) {
670
            $this->response->setStatusCode(404, 'Not Found');
671
            return false;
672
        }
673
        
674
        return $this->setRestResponse($ret['reordered']);
675
    }
676
    
677
    /**
678
     * Sending an error as an http response
679
     *
680
     * @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...
681
     * @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...
682
     *
683
     * @return ResponseInterface
684
     */
685
    public function setRestErrorResponse($code = 400, $status = 'Bad Request', $response = null)
686
    {
687
        return $this->setRestResponse($response, $code, $status);
688
    }
689
    
690
    /**
691
     * Sending rest response as an http response
692
     *
693
     * @param mixed $response
694
     * @param ?int $code
695
     * @param ?string $status
696
     * @param int $jsonOptions
697
     * @param int $depth
698
     *
699
     * @return ResponseInterface
700
     */
701
    public function setRestResponse($response = null, int $code = null, string $status = null, int $jsonOptions = 0, int $depth = 512): ResponseInterface
702
    {
703
        $debug = $this->isDebugEnabled();
704
        
705
        // keep forced status code or set our own
706
        $statusCode = $this->response->getStatusCode();
707
        $reasonPhrase = $this->response->getReasonPhrase();
708
        $code ??= (int)$statusCode ?: 200;
709
        $status ??= $reasonPhrase ?: StatusCode::getMessage($code);
710
        
711
        $view = $this->view->getParamsToView();
712
        $hash = hash('sha512', json_encode($view));
713
        
714
        // set response status code
715
        $this->response->setStatusCode($code, $code . ' ' . $status);
716
        
717
        // @todo handle this correctly
718
        // @todo private vs public cache type
719
        $cache = $this->getCache();
720
        if (!empty($cache['lifetime'])) {
721
            if ($this->response->getStatusCode() === 200) {
722
                $this->response->setCache($cache['lifetime']);
723
                $this->response->setEtag($hash);
724
            }
725
        }
726
        else {
727
            $this->response->setCache(0);
728
            $this->response->setHeader('Cache-Control', 'no-cache, max-age=0');
729
        }
730
        
731
        $ret = [];
732
        $ret['api'] = [];
733
        $ret['api']['version'] = ['0.1']; // @todo
734
        $ret['timestamp'] = date('c');
735
        $ret['hash'] = $hash;
736
        $ret['status'] = $status;
737
        $ret['code'] = $code;
738
        $ret['response'] = $response;
739
        $ret['view'] = $view;
740
        
741
        if ($debug) {
742
            $ret['api']['php'] = phpversion();
743
            $ret['api']['phalcon'] = Version::get();
744
            $ret['api']['zemit'] = $this->config->path('core.version');
745
            $ret['api']['core'] = $this->config->path('core.name');
746
            $ret['api']['app'] = $this->config->path('app.version');
747
            $ret['api']['name'] = $this->config->path('app.name');
748
            
749
            $ret['identity'] = $this->identity ? $this->identity->getIdentity() : null;
750
            $ret['profiler'] = $this->profiler ? $this->profiler->toArray() : null;
751
            $ret['request'] = $this->request ? $this->request->toArray() : null;
752
            $ret['dispatcher'] = $this->dispatcher ? $this->dispatcher->toArray() : null;
753
            $ret['router'] = $this->router ? $this->router->toArray() : null;
754
            $ret['memory'] = Utils::getMemoryUsage();
755
        }
756
        
757
        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

757
        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...
758
    }
759
    
760
    public function beforeExecuteRoute(Dispatcher $dispatcher): void
761
    {
762
        // @todo use eventsManager from service provider instead
763
        $this->eventsManager->enablePriorities(true);
764
        
765
        // @todo see if we can implement receiving an array of responses globally: V2
766
        // $this->eventsManager->collectResponses(true);
767
        
768
        // retrieve events based on the config roles and features
769
        $permissions = $this->config->get('permissions')->toArray() ?? [];
770
        $featureList = $permissions['features'] ?? [];
771
        $roleList = $permissions['roles'] ?? [];
772
        
773
        foreach ($roleList as $role => $rolePermission) {
774
            
775
            if (isset($rolePermission['features'])) {
776
                foreach ($rolePermission['features'] as $feature) {
777
                    $rolePermission = array_merge_recursive($rolePermission, $featureList[$feature] ?? []);
778
                    // @todo remove duplicates
779
                }
780
            }
781
            
782
            $behaviorsContext = $rolePermission['behaviors'] ?? [];
783
            foreach ($behaviorsContext as $className => $behaviors) {
784
                if (is_int($className) || get_class($this) === $className) {
785
                    $this->attachBehaviors($behaviors, 'rest');
786
                }
787
                if ($this->getModelClassName() === $className) {
788
                    $this->attachBehaviors($behaviors, 'model');
789
                }
790
            }
791
        }
792
    }
793
    
794
    /**
795
     * Attach a new behavior
796
     * @todo review behavior type
797
     */
798
    public function attachBehavior(string $behavior, string $eventType = 'rest'): void
799
    {
800
        $event = new $behavior();
801
        
802
        // inject DI
803
        if ($event instanceof Injectable || method_exists($event, 'setDI')) {
804
            $event->setDI($this->getDI());
805
        }
806
        
807
        // attach behavior
808
        $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

808
        $this->eventsManager->/** @scrutinizer ignore-call */ 
809
                              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...
809
    }
810
    
811
    /**
812
     * Attach new behaviors
813
     */
814
    public function attachBehaviors(array $behaviors = [], string $eventType = 'rest'): void
815
    {
816
        foreach ($behaviors as $behavior) {
817
            $this->attachBehavior($behavior, $eventType);
818
        }
819
    }
820
    
821
    /**
822
     * Handle rest response automagically
823
     */
824
    public function afterExecuteRoute(Dispatcher $dispatcher): void
825
    {
826
        $response = $dispatcher->getReturnedValue();
827
        
828
        // Avoid breaking default phalcon behaviour
829
        if ($response instanceof Response) {
830
            return;
831
        }
832
        
833
        // Merge response into view variables
834
        if (is_array($response)) {
835
            $this->view->setVars($response, true);
836
        }
837
        
838
        // Return our Rest normalized response
839
        $dispatcher->setReturnedValue($this->setRestResponse(is_array($response) ? null : $response));
840
    }
841
    
842
    public function isDebugEnabled(): bool
843
    {
844
        return $this->config->path('app.debug') ?? false;
845
    }
846
}
847