Test Failed
Push — master ( b3888b...6b094a )
by Julien
11:38
created

Model::getSoftDeleteCondition()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
cc 1
nc 1
nop 0
crap 2
1
<?php
2
/**
3
 * This file is part of the Zemit Framework.
4
 *
5
 * (c) Zemit Team <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE.txt
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Zemit\Mvc\Controller;
12
13
use Phalcon\Db\Column;
14
use Phalcon\Messages\Message;
15
use Phalcon\Messages\Messages;
16
use Phalcon\Mvc\Model\Resultset;
17
use Phalcon\Mvc\ModelInterface;
18
use Phalcon\Text;
19
use Zemit\Http\Request;
20
use Zemit\Identity;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Zemit\Mvc\Controller\Identity. 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...
21
use Zemit\Mvc\Model\Expose\Expose;
22
use Zemit\Utils\Slug;
23
24
/**
25
 * Trait Model
26
 *
27
 * @author Julien Turbide <[email protected]>
28
 * @copyright Zemit Team <[email protected]>
29
 *
30
 * @since 1.0
31
 * @version 1.0
32
 *
33
 * @package Zemit\Mvc\Controller
34
 */
35
trait Model
36
{
37
    protected $_bind = [];
38
    protected $_bindTypes = [];
39
    
40
    /**
41
     * Get the current Model Name
42
     * @return string|null
43
     * @todo remove for v1
44
     *
45
     * @deprecated change to getModelClassName() instead
46
     */
47
    public function getModelName()
48
    {
49
        return $this->getModelClassName();
50
    }
51
    
52
    /**
53
     * Get the current Model Class Name
54
     *
55
     * @return string|null
56
     */
57
    public function getModelClassName()
58
    {
59
        return $this->getModelNameFromController();
60
    }
61
    
62
    /**
63
     * Get the WhiteList parameters for saving
64
     * @return null|array
65
     * @todo add a whitelist object that would be able to support one configuration for the search, assign, filter
66
     *
67
     */
68
    protected function getWhiteList()
69
    {
70
        return null;
71
    }
72
    
73
    /**
74
     * Get the Flattened WhiteList
75
     *
76
     * @param array|null $whiteList
77
     *
78
     * @return array|null
79
     */
80
    public function getFlatWhiteList(?array $whiteList = null)
81
    {
82
        $whiteList ??= $this->getWhiteList();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getWhiteList() targeting Zemit\Mvc\Controller\Model::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...
83
        $ret = Expose::_parseColumnsRecursive($whiteList);
84
        return $ret ? array_keys($ret) : null;
85
    }
86
    
87
    /**
88
     * Get the WhiteList parameters for filtering
89
     *
90
     * @return null|array
91
     */
92
    protected function getFilterWhiteList()
93
    {
94
        return $this->getWhiteList();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getWhiteList() targeting Zemit\Mvc\Controller\Model::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...
95
    }
96
    
97
    /**
98
     * Get the WhiteList parameters for filtering
99
     *
100
     * @return null|array
101
     */
102
    protected function getSearchWhiteList()
103
    {
104
        return $this->getFilterWhiteList();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getFilterWhiteList() targeting Zemit\Mvc\Controller\Model::getFilterWhiteList() 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...
105
    }
106
    
107
    /**
108
     * Get the column mapping for crud
109
     *
110
     * @return null|array
111
     */
112
    protected function getColumnMap()
113
    {
114
        return null;
115
    }
116
    
117
    /**
118
     * Get relationship eager loading definition
119
     *
120
     * @return null|array
121
     */
122
    protected function getWith()
123
    {
124
        return null;
125
    }
126
    
127
    /**
128
     * Get relationship eager loading definition for a listing
129
     *
130
     * @return null|array
131
     */
132
    protected function getListWith()
133
    {
134
        return $this->getWith();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getWith() targeting Zemit\Mvc\Controller\Model::getWith() 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...
135
    }
136
    
137
    /**
138
     * Get expose definition for a single entity
139
     *
140
     * @return null|array
141
     */
142
    protected function getExpose()
143
    {
144
        return null;
145
    }
146
    
147
    /**
148
     * Get expose definition for listing many entities
149
     *
150
     * @return null|array
151
     */
152
    protected function getListExpose()
153
    {
154
        return $this->getExpose();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getExpose() targeting Zemit\Mvc\Controller\Model::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...
155
    }
156
    
157
    /**
158
     * Get expose definition for export
159
     *
160
     * @return null|array
161
     */
162
    protected function getExportExpose()
163
    {
164
        return $this->getExpose();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getExpose() targeting Zemit\Mvc\Controller\Model::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...
165
    }
166
    
167
    /**
168
     * Get columns merge definition for export
169
     *
170
     * @return null|array
171
     */
172
    public function getExportMergeColum()
173
    {
174
        return null;
175
    }
176
    
177
    /**
178
     * Get columns format field text definition for export
179
     *
180
     * @param array|null $params
181
     *
182
     * @return null|array
183
     */
184
    public function getExportFormatFieldText(?array $params = null)
185
    {
186
        return null;
187
    }
188
    
189
    /**
190
     * Get join definition
191
     *
192
     * @return null|array
193
     */
194
    protected function getJoins()
195
    {
196
        return null;
197
    }
198
    
199
    /**
200
     * Get the order definition
201
     *
202
     * @return null|array
203
     */
204
    protected function getOrder()
205
    {
206
        return $this->getParamExplodeArrayMapFilter('order');
207
    }
208
    
209
    /**
210
     * Get the current limit value
211
     *
212
     * @return null|int Default: 1000
213
     */
214
    protected function getLimit(): int
215
    {
216
        return (int)$this->getParam('limit', 'int', 1000);
217
    }
218
    
219
    /**
220
     * Get the current offset value
221
     *
222
     * @return null|int Default: 0
223
     */
224
    protected function getOffset(): int
225
    {
226
        return (int)$this->getParam('offset', 'int', 0);
227
    }
228
    
229
    /**
230
     * Get group
231
     * - Automatically group by ID by default if nothing else is provided
232
     * - This will fix multiple single records being returned for the same model with joins
233
     *
234
     * @return array[string]|string|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment array[string]|string|null at position 1 could not be parsed: Expected ']' at position 1, but found '['.
Loading history...
235
     */
236
    protected function getGroup()
237
    {
238
        $group = $this->getParamExplodeArrayMapFilter('group');
239
        
240
        // Fix for joins, automatically append grouping if none provided
241
        $join = $this->getJoins();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $join is correct as $this->getJoins() targeting Zemit\Mvc\Controller\Model::getJoins() 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...
242
        if (empty($group) && !empty($join)) {
243
            $group = $this->appendModelName('id');
244
        }
245
        
246
        return $group;
247
    }
248
    
249
    /**
250
     * Get distinct
251
     * @TODO see how to implement this, maybe an action itself
252
     *
253
     * @return array[string]|string|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment array[string]|string|null at position 1 could not be parsed: Expected ']' at position 1, but found '['.
Loading history...
254
     */
255
    protected function getDistinct()
256
    {
257
        return $this->getParamExplodeArrayMapFilter('distinct');
258
    }
259
    
260
    /**
261
     * Get columns
262
     * @TODO see how to implement this
263
     *
264
     * @return array[string]|string|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment array[string]|string|null at position 1 could not be parsed: Expected ']' at position 1, but found '['.
Loading history...
265
     */
266
    protected function getColumns()
267
    {
268
        return $this->getParam('columns', 'string');
269
    }
270
    
271
    /**
272
     * Return the whitelisted role list for the current model
273
     *
274
     * @return string[] By default will return dev and admin role
275
     */
276
    protected function getRoleList()
277
    {
278
        return ['dev', 'admin'];
279
    }
280
    
281
    /**
282
     * Get Search condition
283
     *
284
     * @return string Default: deleted = 0
285
     */
286
    protected function getSearchCondition()
287
    {
288
        $conditions = [];
289
        
290
        $searchList = array_values(array_filter(array_unique(explode(' ', $this->getParam('search', 'string')))));
0 ignored issues
show
Bug introduced by
It seems like $this->getParam('search', 'string') can also be of type null and string[]; however, parameter $string of explode() does only seem to accept string, 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

290
        $searchList = array_values(array_filter(array_unique(explode(' ', /** @scrutinizer ignore-type */ $this->getParam('search', 'string')))));
Loading history...
291
        
292
        foreach ($searchList as $searchTerm) {
293
            $orConditions = [];
294
            $searchWhiteList = $this->getSearchWhiteList();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $searchWhiteList is correct as $this->getSearchWhiteList() targeting Zemit\Mvc\Controller\Model::getSearchWhiteList() 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...
295
            if ($searchWhiteList) {
296
                foreach ($searchWhiteList as $whiteList) {
0 ignored issues
show
Bug introduced by
The expression $searchWhiteList of type void is not traversable.
Loading history...
297
                    
298
                    // Multidimensional arrays not supported yet
299
                    // @todo support this properly
300
                    if (is_array($whiteList)) {
301
                        continue;
302
                    }
303
                    
304
                    $searchTermBinding = '_' . uniqid() . '_';
305
                    $orConditions [] = $this->appendModelName($whiteList) . " like :$searchTermBinding:";
0 ignored issues
show
Bug introduced by
Are you sure $this->appendModelName($whiteList) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

305
                    $orConditions [] = /** @scrutinizer ignore-type */ $this->appendModelName($whiteList) . " like :$searchTermBinding:";
Loading history...
306
                    $this->setBind([$searchTermBinding => '%' . $searchTerm . '%']);
307
                    $this->setBindTypes([$searchTermBinding => Column::BIND_PARAM_STR]);
308
                }
309
            }
310
            
311
            if (!empty($orConditions)) {
312
                $conditions [] = '(' . implode(' or ', $orConditions) . ')';
313
            }
314
        }
315
        
316
        return empty($conditions) ? null : '(' . implode(' and ', $conditions) . ')';
317
    }
318
    
319
    /**
320
     * Get Soft delete condition
321
     *
322
     * @return string Default: deleted = 0
323
     */
324
    protected function getSoftDeleteCondition(): ?string
325
    {
326
        return '[' . $this->getModelClassName() . '].[deleted] = 0';
327
    }
328
    
329
    /**
330
     * @param $field
331
     * @param string $sanitizer
332
     * @param string $glue
333
     *
334
     * @return array|string[]
335
     */
336
    public function getParamExplodeArrayMapFilter($field, $sanitizer = 'string', $glue = ',')
337
    {
338
        $filter = $this->filter;
339
        $ret = array_filter(array_map(function ($e) use ($filter, $sanitizer) {
340
            return $this->appendModelName(trim($filter->sanitize($e, $sanitizer)));
341
        }, explode($glue, $this->getParam($field, $sanitizer))));
0 ignored issues
show
Bug introduced by
It seems like $this->getParam($field, $sanitizer) can also be of type null and string[]; however, parameter $string of explode() does only seem to accept string, 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

341
        }, explode($glue, /** @scrutinizer ignore-type */ $this->getParam($field, $sanitizer))));
Loading history...
342
        
343
        return empty($ret) ? null : $ret;
344
    }
345
    
346
    /**
347
     * Set the variables to bind
348
     *
349
     * @param array $bind Variable bind to merge or replace
350
     * @param bool $replace Pass true to replace the entire bind set
351
     */
352
    public function setBind(array $bind = [], bool $replace = false)
353
    {
354
        $this->_bind = $replace ? $bind : array_merge($this->getBind(), $bind);
0 ignored issues
show
Bug introduced by
It seems like $this->getBind() can also be of type null; however, parameter $arrays of array_merge() 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

354
        $this->_bind = $replace ? $bind : array_merge(/** @scrutinizer ignore-type */ $this->getBind(), $bind);
Loading history...
355
    }
356
    
357
    /**
358
     * Get the current bind
359
     * key => value
360
     *
361
     * @return array|null
362
     */
363
    public function getBind()
364
    {
365
        return $this->_bind ?? null;
366
    }
367
    
368
    /**
369
     * Set the variables types to bind
370
     *
371
     * @param array $bindTypes Variable bind types to merge or replace
372
     * @param bool $replace Pass true to replace the entire bind type set
373
     */
374
    public function setBindTypes(array $bindTypes = [], bool $replace = false)
375
    {
376
        $this->_bindTypes = $replace ? $bindTypes : array_merge($this->getBindTypes(), $bindTypes);
0 ignored issues
show
Bug introduced by
It seems like $this->getBindTypes() can also be of type null; however, parameter $arrays of array_merge() 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

376
        $this->_bindTypes = $replace ? $bindTypes : array_merge(/** @scrutinizer ignore-type */ $this->getBindTypes(), $bindTypes);
Loading history...
377
    }
378
    
379
    /**
380
     * Get the current bind types
381
     *
382
     * @return array|null
383
     */
384
    public function getBindTypes()
385
    {
386
        return $this->_bindTypes ?? null;
387
    }
388
    
389
    /**
390
     * Get Created By Condition
391
     *
392
     * @param string[] $columns
393
     * @param Identity|null $identity
394
     * @param string[]|null $roleList
395
     *
396
     * @return null
397
     *
398
     * @return string|null
399
     */
400
    protected function getIdentityCondition(array $columns = null, Identity $identity = null, $roleList = null)
401
    {
402
        // @todo
403
        if ($this->request->isOptions()) {
404
            return null;
405
        }
406
        
407
        $identity ??= $this->identity ?? false;
408
        $roleList ??= $this->getRoleList();
409
        $modelName = $this->getModelClassName();
410
        
411
        if ($modelName && $identity && !$identity->hasRole($roleList)) {
412
            $ret = [];
413
            
414
            $columns ??= [
415
                'createdBy',
416
                'ownedBy',
417
                'userId',
418
            ];
419
            
420
            foreach ($columns as $column) {
421
                if (!property_exists($modelName, $column)) {
422
                    continue;
423
                }
424
                
425
                $field = strpos($column, '.') !== false ? $column : $modelName . '.' . $column;
426
                $field = '[' . str_replace('.', '].[', $field) . ']';
427
                
428
                $this->setBind([$column => (int)$identity->getUserId()]);
429
                $this->setBindTypes([$column => Column::BIND_PARAM_INT]);
430
                $ret [] = $field . ' = :' . $column . ':';
431
            }
432
            
433
            return implode(' or ', $ret);
0 ignored issues
show
Bug Best Practice introduced by
The expression return implode(' or ', $ret) returns the type string which is incompatible with the documented return type null.
Loading history...
434
        }
435
        
436
        return null;
437
    }
438
    
439
    function arrayMapRecursive($callback, $array)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
Comprehensibility Best Practice introduced by
It is recommend to declare an explicit visibility for arrayMapRecursive.

Generally, we recommend to declare visibility for all methods in your source code. This has the advantage of clearly communication to other developers, and also yourself, how this method should be consumed.

If you are not sure which visibility to choose, it is a good idea to start with the most restrictive visibility, and then raise visibility as needed, i.e. start with private, and only raise it to protected if a sub-class needs to have access, or public if an external class needs access.

Loading history...
440
    {
441
        $func = function ($item) use (&$func, &$callback) {
442
            return is_array($item) ? array_map($func, $item) : call_user_func($callback, $item);
443
        };
444
        
445
        return array_map($func, $array);
446
    }
447
    
448
    /**
449
     * Get Filter Condition
450
     *
451
     * @param array|null $filters
452
     * @param array|null $whiteList
453
     * @param bool $or
454
     *
455
     * @return string|null Return the generated query
456
     * @throws \Exception Throw an exception if the field property is not valid
457
     * @todo escape fields properly
458
     *
459
     */
460
    protected function getFilterCondition(array $filters = null, array $whiteList = null, $or = false)
461
    {
462
        $filters ??= $this->getParam('filters');
463
        $whiteList ??= $this->getFilterWhiteList();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getFilterWhiteList() targeting Zemit\Mvc\Controller\Model::getFilterWhiteList() 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...
464
        $whiteList = $this->getFlatWhiteList($whiteList);
465
        $lowercaseWhiteList = !is_null($whiteList) ? $this->arrayMapRecursive('mb_strtolower', $whiteList) : $whiteList;
466
        
467
        // No filter, no query
468
        if (empty($filters)) {
469
            return null;
470
        }
471
        
472
        $query = [];
473
        foreach ($filters as $filter) {
474
            $field = $this->filter->sanitize($filter['field'] ?? null, ['string', 'trim']);
475
            
476
            if (!empty($field)) {
477
                $lowercaseField = mb_strtolower($field);
478
                
479
                // whiteList on filter condition
480
                if (is_null($whiteList) || !in_array($lowercaseField, $lowercaseWhiteList, true)) {
0 ignored issues
show
Bug introduced by
It seems like $lowercaseWhiteList can also be of type null; however, parameter $haystack of in_array() 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

480
                if (is_null($whiteList) || !in_array($lowercaseField, /** @scrutinizer ignore-type */ $lowercaseWhiteList, true)) {
Loading history...
481
                    // @todo if config is set to throw exception on usage of not allowed filters otherwise continue looping through
482
                    throw new \Exception('Not allowed to filter using the following field: `' . $field . '`', 403);
483
                }
484
                
485
                $uniqid = substr(md5(json_encode($filter)), 0, 10);
486
//                $queryField = '_' . uniqid($uniqid . '_field_') . '_';
487
                $queryValue = '_' . uniqid($uniqid . '_value_') . '_';
488
                $queryOperator = strtolower($filter['operator']);
489
    
490
                // Map alias query operator
491
                $mapAlias = [
492
                    'equals' => '=',
493
                    'does not equal' => '!=',
494
                    'different than' => '<>',
495
                    'greater than' => '>',
496
                    'greater than or equal' => '>=',
497
                    'less than' => '<',
498
                    'less than or equal' => '<=',
499
                    'null-safe equal' => '<=>',
500
                ];
501
                $queryOperator = $mapAlias[$queryOperator] ?? $queryOperator;
502
                
503
                switch ($queryOperator) {
504
                    
505
                    // mysql native
506
                    case '=': // Equal operator
507
                    case '!=': // Not equal operator
508
                    case '<>': // Not equal operator
509
                    case '>': // Greater than operator
510
                    case '>=': // Greater than or equal operator
511
                    case '<': // Less than or equal operator
512
                    case '<=': // Less than or equal operator
513
                    case '<=>': // NULL-safe equal to operator
514
                    case 'in': // Whether a value is within a set of values
515
                    case 'not in': // Whether a value is not within a set of values
516
                    case 'like': // Simple pattern matching
517
                    case 'not like': // Negation of simple pattern matching
518
                    case 'between': // Whether a value is within a range of values
519
                    case 'not between': // Whether a value is not within a range of values
520
                    case 'is': // Test a value against a boolean
521
                    case 'is not': // Test a value against a boolean
522
                    case 'is null': // NULL value test
523
                    case 'is not null': // NOT NULL value test
524
                    case 'is false': // Test a value against a boolean
525
                    case 'is not false': // // Test a value against a boolean
526
                    case 'is true': // // Test a value against a boolean
527
                    case 'is not true': // // Test a value against a boolean
528
                        break;
529
                        
530
                    // advanced filters
531
                    case 'regexp': // @todo
532
                    case 'not regexp': // @todo
533
                    case 'contains':
534
                    case 'does not contain':
535
                    case 'contains word': // @todo
536
                    case 'does not contain word': // @todo
537
                    case 'distance sphere greater than'; // @todo
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
538
                    case 'distance sphere greater than or equal'; // @todo
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
539
                    case 'distance sphere less than'; // @todo
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
540
                    case 'distance sphere less than or equal'; // @todo
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
541
                    case 'is empty':
542
                    case 'is not empty':
543
                        break;
544
                        
545
                    default:
546
                        throw new \Exception('Not allowed to filter using the following operator: `' . $queryOperator . '`', 403);
547
                }
548
                
549
                $bind = [];
550
                $bindType = [];
551
552
//                $bind[$queryField] = $filter['field'];
553
//                $bindType[$queryField] = Column::BIND_PARAM_STR;
554
//                $queryFieldBinder = ':' . $queryField . ':';
555
//                $queryFieldBinder = '{' . $queryField . '}';
556
                
557
                // Add the current model name by default
558
                $field = $this->appendModelName($field);
559
                
560
                $queryFieldBinder = $field;
561
                $queryValueBinder = ':' . $queryValue . ':';
562
                if (isset($filter['value'])) {
563
                    // special for between and not between
564
                    if (in_array($queryOperator, ['between', 'not between'])) {
565
                        $queryValue0 = '_' . uniqid($uniqid . '_value_') . '_';
566
                        $queryValue1 = '_' . uniqid($uniqid . '_value_') . '_';
567
                        $bind[$queryValue0] = $filter['value'][0];
568
                        $bind[$queryValue1] = $filter['value'][1];
569
                        $bindType[$queryValue0] = Column::BIND_PARAM_STR;
570
                        $bindType[$queryValue1] = Column::BIND_PARAM_STR;
571
                        $query [] = (($queryOperator === 'not between') ? 'not ' : null) . "$queryFieldBinder between :$queryValue0: and :$queryValue1:";
572
                    }
573
                    
574
                    else if (in_array($queryOperator, ['contains', 'does not contain'])) {
575
                        $queryValue0 = '_' . uniqid($uniqid . '_value_') . '_';
576
                        $queryValue1 = '_' . uniqid($uniqid . '_value_') . '_';
577
                        $queryValue2 = '_' . uniqid($uniqid . '_value_') . '_';
578
                        $bind[$queryValue0] = '%' . $filter['value'] . '%';
579
                        $bind[$queryValue1] = '%' . $filter['value'];
580
                        $bind[$queryValue2] = $filter['value'] . '%';
581
                        $bindType[$queryValue0] = Column::BIND_PARAM_STR;
582
                        $bindType[$queryValue1] = Column::BIND_PARAM_STR;
583
                        $bindType[$queryValue2] = Column::BIND_PARAM_STR;
584
                        $query [] = ($queryOperator === 'does not contain'? '!' : '') . "($queryFieldBinder like :$queryValue0: or $queryFieldBinder like :$queryValue1: or $queryFieldBinder like :$queryValue2:)";
585
                    }
586
                    
587
                    else if (in_array($queryOperator, ['is empty', 'is not empty'])) {
588
                        $query [] = ($queryOperator === 'is not empty'? '!' : '') . "(TRIM($queryFieldBinder) = '' or $queryFieldBinder is null)";
589
                    }
590
                    
591
                    else if (in_array($queryOperator, ['regexp', 'not regexp'])) {
592
                        $bind[$queryValue] = $filter['value'];
593
                        $query [] = $queryOperator . "REGEXP($queryFieldBinder, $queryValueBinder)";
594
                    }
595
                    
596
                    else if (in_array($queryOperator, [
597
                        'distance sphere equals',
598
                        'distance sphere greater than',
599
                        'distance sphere greater than or equal',
600
                        'distance sphere less than',
601
                        'distance sphere less than or equal',
602
                    ])) {
603
                        // Prepare values binding of 2 sphere point to calculate distance
604
                        $queryBindValue0 = '_' . uniqid($uniqid . '_value_') . '_';
605
                        $queryBindValue1 = '_' . uniqid($uniqid . '_value_') . '_';
606
                        $queryBindValue2 = '_' . uniqid($uniqid . '_value_') . '_';
607
                        $queryBindValue3 = '_' . uniqid($uniqid . '_value_') . '_';
608
                        $bind[$queryBindValue0] = $filter['value'][0];
609
                        $bind[$queryBindValue1] = $filter['value'][1];
610
                        $bind[$queryBindValue2] = $filter['value'][2];
611
                        $bind[$queryBindValue3] = $filter['value'][3];
612
                        $bindType[$queryBindValue0] = Column::BIND_PARAM_DECIMAL;
613
                        $bindType[$queryBindValue1] = Column::BIND_PARAM_DECIMAL;
614
                        $bindType[$queryBindValue2] = Column::BIND_PARAM_DECIMAL;
615
                        $bindType[$queryBindValue3] = Column::BIND_PARAM_DECIMAL;
616
    
617
                        $queryPointLatBinder0 = $queryBindValue0;
618
                        $queryPointLonBinder0 = $queryBindValue1;
619
                        $queryPointLatBinder1 = $queryBindValue2;
620
                        $queryPointLonBinder1 = $queryBindValue3;
621
    
622
                        $queryLogicalOperator =
623
                            (strpos($queryOperator, 'greater') !== false? '>' : null) .
624
                            (strpos($queryOperator, 'less') !== false? '<' : null) .
625
                            (strpos($queryOperator, 'equal') !== false? '=' : null);
626
    
627
                        $bind[$queryValue] = $filter['value'];
628
                        $query [] = "ST_Distance_Sphere(point($queryPointLatBinder0, $queryPointLonBinder0), point($queryPointLatBinder1, $queryPointLonBinder1)) $queryLogicalOperator $queryValueBinder";
629
                    }
630
                    
631
                    else {
632
                        $bind[$queryValue] = $filter['value'];
633
                        if (is_string($filter['value'])) {
634
                            $bindType[$queryValue] = Column::BIND_PARAM_STR;
635
                        }
636
                        else if (is_int($filter['value'])) {
637
                            $bindType[$queryValue] = Column::BIND_PARAM_INT;
638
                        }
639
                        else if (is_bool($filter['value'])) {
640
                            $bindType[$queryValue] = Column::BIND_PARAM_BOOL;
641
                        }
642
                        else if (is_float($filter['value'])) {
643
                            $bindType[$queryValue] = Column::BIND_PARAM_DECIMAL;
644
                        }
645
                        else if (is_double($filter['value'])) {
646
                            $bindType[$queryValue] = Column::BIND_PARAM_DECIMAL;
647
                        }
648
                        else if (is_array($filter['value'])) {
649
                            $queryValueBinder = '({' . $queryValue . ':array})';
650
                            $bindType[$queryValue] = Column::BIND_PARAM_STR;
651
                        }
652
                        else {
653
                            $bindType[$queryValue] = Column::BIND_PARAM_NULL;
654
                        }
655
                        $query [] = "$queryFieldBinder $queryOperator $queryValueBinder";
656
                    }
657
                }
658
                else {
659
                    $query [] = "$queryFieldBinder $queryOperator";
660
                }
661
                
662
                $this->setBind($bind);
663
                $this->setBindTypes($bindType);
664
            }
665
            else {
666
                if (is_array($filter) || $filter instanceof \Traversable) {
667
                    $query [] = $this->getFilterCondition($filter, $whiteList, !$or);
0 ignored issues
show
Bug introduced by
It seems like $filter can also be of type Traversable; however, parameter $filters of Zemit\Mvc\Controller\Model::getFilterCondition() does only seem to accept array|null, 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

667
                    $query [] = $this->getFilterCondition(/** @scrutinizer ignore-type */ $filter, $whiteList, !$or);
Loading history...
668
                }
669
                else {
670
                    throw new \Exception('A valid field property is required.', 400);
671
                }
672
            }
673
        }
674
        
675
        return empty($query) ? null : '(' . implode($or ? ' or ' : ' and ', $query) . ')';
676
    }
677
    
678
    /**
679
     * Append the current model name alias to the field
680
     * So: field -> [Alias].[field]
681
     *
682
     * @param string|array $field
683
     * @param null $modelName
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $modelName is correct as it would always require null to be passed?
Loading history...
684
     *
685
     * @return array|string
686
     */
687
    public function appendModelName($field, $modelName = null)
688
    {
689
        $modelName ??= $this->getModelClassName();
690
        
691
        if (empty($field)) {
692
            return $field;
693
        }
694
        
695
        if (is_string($field)) {
696
            // Add the current model name by default
697
            $explode = explode(' ', $field);
698
            if (!strpos($field, '.') !== false) {
699
                $field = trim('[' . $modelName . '].[' . array_shift($explode) . '] ' . implode(' ', $explode));
700
            }
701
            else if (strpos($field, ']') === false && strpos($field, '[') === false) {
702
                $field = trim('[' . implode('].[', explode('.', array_shift($explode))) . ']' . implode(' ', $explode));
703
            }
704
        }
705
        else if (is_array($field)) {
0 ignored issues
show
introduced by
The condition is_array($field) is always true.
Loading history...
706
            foreach ($field as $fieldKey => $fieldValue) {
707
                $field[$fieldKey] = $this->appendModelName($fieldValue, $modelName);
0 ignored issues
show
Bug introduced by
It seems like $modelName can also be of type string; however, parameter $modelName of Zemit\Mvc\Controller\Model::appendModelName() does only seem to accept null, 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

707
                $field[$fieldKey] = $this->appendModelName($fieldValue, /** @scrutinizer ignore-type */ $modelName);
Loading history...
708
            }
709
        }
710
        
711
        return $field;
712
    }
713
    
714
    /**
715
     * Get Permission Condition
716
     *
717
     * @return null
718
     */
719
    protected function getPermissionCondition($type = null, $identity = null)
720
    {
721
        return null;
722
    }
723
    
724
    protected function fireGet($method)
725
    {
726
        $ret = $this->{$method}();
727
        $eventRet = $this->eventsManager->fire('rest:' . $method, $this, $ret);
728
        return $eventRet === false ? null : $eventRet ?? $ret;
729
    }
730
    
731
    /**
732
     * Get all conditions
733
     *
734
     * @return string
735
     * @throws \Exception
736
     */
737
    protected function getConditions()
738
    {
739
        $conditions = array_values(array_unique(array_filter([
740
            $this->fireGet('getSoftDeleteCondition'),
741
            $this->fireGet('getIdentityCondition'),
742
            $this->fireGet('getFilterCondition'),
743
            $this->fireGet('getSearchCondition'),
744
            $this->fireGet('getPermissionCondition'),
745
        ])));
746
        
747
        if (empty($conditions)) {
748
            $conditions [] = 1;
749
        }
750
        
751
        return '(' . implode(') and (', $conditions) . ')';
752
    }
753
    
754
    /**
755
     * Get having conditions
756
     */
757
    public function getHaving()
758
    {
759
        return null;
760
    }
761
    
762
    /**
763
     * Get a cache key from params
764
     *
765
     * @param array|null $params
766
     *
767
     * @return string|null
768
     */
769
    public function getCacheKey(?array $params = null): ?string
770
    {
771
        $params ??= $this->getParams();
772
        
773
        return Slug::generate(json_encode($params, JSON_UNESCAPED_SLASHES));
774
    }
775
    
776
    /**
777
     * Get cache setting
778
     *
779
     * @param array|null $params
780
     *
781
     * @return array|null
782
     */
783
    public function getCache(?array $params = null)
784
    {
785
        $params ??= $this->getParams();
786
        
787
        if (!empty($params['cache'])) {
788
            return [
789
                'lifetime' => (int)$params['cache'],
790
                'key' => $this->getCacheKey($params),
791
            ];
792
        }
793
        
794
        return null;
795
    }
796
    
797
    
798
    /**
799
     * Get requested content type
800
     * - Default will return csv
801
     *
802
     * @param array|null $params
803
     *
804
     * @return string
805
     * @throws \Exception
806
     */
807
    public function getContentType(?array $params = null)
808
    {
809
        $params ??= $this->getParams();
810
        
811
        $contentType = strtolower($params['contentType'] ?? $params['content-type'] ?? 'json');
812
        
813
        switch ($contentType) {
814
            case 'html':
815
            case 'text/html':
816
            case 'application/html':
817
                // html not supported yet
818
                break;
819
            case 'xml':
820
            case 'text/xml':
821
            case 'application/xml':
822
                // xml not supported yet
823
                break;
824
            case 'text':
825
            case 'text/plain':
826
                // plain text not supported yet
827
                break;
828
            case 'json':
829
            case 'text/json':
830
            case 'application/json':
831
                return 'json';
832
            case 'csv':
833
            case 'text/csv':
834
                return 'csv';
835
            case 'xlsx':
836
            case 'application/xlsx':
837
            case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
838
                return 'xlsx';
839
            case 'xls':
840
            case 'application/vnd.ms-excel':
841
                // old xls not supported yet
842
                break;
843
        }
844
        
845
        throw new \Exception('`' . $contentType . '` is not supported.', 400);
846
    }
847
    
848
    /**
849
     * Get find definition
850
     *
851
     * @return array
852
     * @throws \Exception
853
     */
854
    protected function getFind()
855
    {
856
        $find = [];
857
        $find['conditions'] = $this->fireGet('getConditions');
858
        $find['bind'] = $this->fireGet('getBind');
859
        $find['bindTypes'] = $this->fireGet('getBindTypes');
860
        $find['limit'] = $this->fireGet('getLimit');
861
        $find['offset'] = $this->fireGet('getOffset');
862
        $find['order'] = $this->fireGet('getOrder');
863
        $find['columns'] = $this->fireGet('getColumns');
864
        $find['distinct'] = $this->fireGet('getDistinct');
865
        $find['joins'] = $this->fireGet('getJoins');
866
        $find['group'] = $this->fireGet('getGroup');
867
        $find['having'] = $this->fireGet('getHaving');
868
        $find['cache'] = $this->fireGet('getCache');
869
        
870
        // fix for grouping by multiple fields, phalcon only allow string here
871
        foreach (['distinct', 'group'] as $findKey) {
872
            if (isset($find[$findKey]) && is_array($find[$findKey])) {
873
                $find[$findKey] = implode(', ', $find[$findKey]);
874
            }
875
        }
876
        
877
        return array_filter($find);
878
    }
879
    
880
    /**
881
     * Return find lazy loading config for count
882
     * @return array|string
883
     */
884
    protected function getFindCount($find = null)
885
    {
886
        $find ??= $this->getFind();
887
        if (isset($find['limit'])) {
888
            unset($find['limit']);
889
        }
890
        if (isset($find['offset'])) {
891
            unset($find['offset']);
892
        }
893
//        if (isset($find['group'])) {
894
//            unset($find['group']);
895
//        }
896
        
897
        return array_filter($find);
898
    }
899
    
900
    /**
901
     * @param string $key
902
     * @param string[]|string|null $filters
903
     * @param string|null $default
904
     * @param array|null $params
905
     *
906
     * @return string[]|string|null
907
     */
908
    public function getParam(string $key, $filters = null, string $default = null, array $params = null)
909
    {
910
        $params ??= $this->getParams();
911
        
912
        return $this->filter->sanitize($params[$key] ?? $this->dispatcher->getParam($key, $filters, $default), $filters);
913
    }
914
    
915
    /**
916
     * Get parameters from
917
     * - JsonRawBody, post, put or get
918
     * @return mixed
919
     */
920
    protected function getParams(array $filters = null)
921
    {
922
        /** @var Request $request */
923
        $request = $this->request;
924
        
925
        if (!empty($filters)) {
926
            foreach ($filters as $filter) {
927
                $request->setParameterFilters($filter['name'], $filter['filters'], $filter['scope']);
928
            }
929
        }
930
931
//        $params = empty($request->getRawBody()) ? [] : $request->getJsonRawBody(true); // @TODO handle this differently
932
        return array_merge_recursive(
933
            $request->getFilteredQuery(), // $_GET
934
            $request->getFilteredPut(), // $_PUT
935
            $request->getFilteredPost(), // $_POST
936
        );
937
    }
938
    
939
    /**
940
     * Get Single from ID and Model Name
941
     *
942
     * @param string|int|null $id
943
     * @param string|null $modelName
944
     * @param string|array|null $with
945
     *
946
     * @return bool|Resultset|\Zemit\Mvc\Model
947
     */
948
    public function getSingle($id = null, $modelName = null, $with = [], $find = null, $appendCondition = true)
949
    {
950
        $id ??= (int)$this->getParam('id', 'int');
951
        $modelName ??= $this->getModelClassName();
952
        $with ??= $this->getWith();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getWith() targeting Zemit\Mvc\Controller\Model::getWith() 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...
953
        $find ??= $this->getFind();
954
        $condition = '[' . $modelName . '].[id] = ' . (int)$id;
955
        if ($appendCondition) {
956
            $find['conditions'] .= (empty($find['conditions']) ? null : ' and ') . $condition;
957
        }
958
        else {
959
            $find['bind'] = [];
960
            $find['bindTypes'] = [];
961
            $find['conditions'] = $condition;
962
        }
963
        
964
        return $id ? $modelName::findFirstWith($with ?? [], $find ?? []) : false;
0 ignored issues
show
Bug introduced by
The method findFirstWith() does not exist on null. ( Ignorable by Annotation )

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

964
        return $id ? $modelName::/** @scrutinizer ignore-call */ findFirstWith($with ?? [], $find ?? []) : 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...
965
    }
966
    
967
    /**
968
     * Saving model automagically
969
     *
970
     * Note:
971
     * If a newly created entity can't be retrieved using the ->getSingle
972
     * method after it's creation, the entity will be returned directly
973
     *
974
     * @TODO Support Composite Primary Key
975
     *
976
     * @param null|int|string $id
977
     * @param null|\Zemit\Mvc\Model $entity
978
     * @param null|mixed $post
979
     * @param null|string $modelName
980
     * @param null|array $whiteList
981
     * @param null|array $columnMap
982
     * @param null|array $with
983
     *
984
     * @return array
985
     */
986
    protected function save($id = null, $entity = null, $post = null, $modelName = null, $whiteList = null, $columnMap = null, $with = null)
987
    {
988
        $single = false;
989
        $retList = [];
990
        
991
        // Get the model name to play with
992
        $modelName ??= $this->getModelClassName();
993
        $post ??= $this->getParams();
994
        $whiteList ??= $this->getWhiteList();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getWhiteList() targeting Zemit\Mvc\Controller\Model::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...
995
        $columnMap ??= $this->getColumnMap();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getColumnMap() targeting Zemit\Mvc\Controller\Model::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...
996
        $with ??= $this->getWith();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getWith() targeting Zemit\Mvc\Controller\Model::getWith() 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...
997
        $id = (int)$id;
998
        
999
        // Check if multi-d post
1000
        if (!empty($id) || !isset($post[0]) || !is_array($post[0])) {
1001
            $single = true;
1002
            $post = [$post];
1003
        }
1004
        
1005
        // Save each posts
1006
        foreach ($post as $key => $singlePost) {
1007
            $ret = [];
1008
            
1009
            // @todo see if we should remove this earlier
1010
            if (isset($singlePost['_url'])) {
1011
                unset($singlePost['_url']);
1012
            }
1013
            
1014
            $singlePostId = (!$single || empty($id)) ? $this->getParam('id', 'int', $this->getParam('int', 'int', null)) : $id;
0 ignored issues
show
Bug introduced by
It seems like $this->getParam('int', 'int', null) can also be of type string[]; however, parameter $default of Zemit\Mvc\Controller\Model::getParam() does only seem to accept null|string, 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

1014
            $singlePostId = (!$single || empty($id)) ? $this->getParam('id', 'int', /** @scrutinizer ignore-type */ $this->getParam('int', 'int', null)) : $id;
Loading history...
1015
            unset($singlePost['id']);
1016
            
1017
            /** @var \Zemit\Mvc\Model $singlePostEntity */
1018
            $singlePostEntity = (!$single || !isset($entity)) ? $this->getSingle($singlePostId, $modelName) : $entity;
0 ignored issues
show
Bug introduced by
It seems like $singlePostId can also be of type string[]; however, parameter $id of Zemit\Mvc\Controller\Model::getSingle() does only seem to accept integer|null|string, 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

1018
            $singlePostEntity = (!$single || !isset($entity)) ? $this->getSingle(/** @scrutinizer ignore-type */ $singlePostId, $modelName) : $entity;
Loading history...
1019
            
1020
            // Create entity if not exists
1021
            if (!$singlePostEntity && empty($singlePostId)) {
1022
                $singlePostEntity = new $modelName();
1023
            }
1024
            
1025
            if (!$singlePostEntity) {
1026
                $ret = [
1027
                    'saved' => false,
1028
                    'messages' => [new Message('Entity id `' . $singlePostId . '` not found.', $modelName, 'NotFound', 404)],
1029
                    'model' => $modelName,
1030
                    'source' => (new $modelName)->getSource(),
1031
                ];
1032
            }
1033
            else {
1034
                // allow custom manipulations
1035
                // @todo move this using events
1036
                $this->beforeAssign($singlePostEntity, $singlePost, $whiteList, $columnMap);
1037
                
1038
                // assign & save
1039
                $singlePostEntity->assign($singlePost, $whiteList, $columnMap);
1040
                $ret = $this->saveEntity($singlePostEntity);
1041
                
1042
                // refetch & expose
1043
                $fetch = $this->getSingle($singlePostEntity->getId(), $modelName, $with);
1044
                $ret[$single ? 'single' : 'list'] = $fetch ? $fetch->expose($this->getExpose()) : false;
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getExpose() targeting Zemit\Mvc\Controller\Model::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...
Bug introduced by
The method expose() 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

1044
                $ret[$single ? 'single' : 'list'] = $fetch ? $fetch->/** @scrutinizer ignore-call */ expose($this->getExpose()) : 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...
1045
            }
1046
            
1047
            $retList [] = $ret;
1048
        }
1049
        
1050
        return $single ? $retList[0] : $retList;
1051
    }
1052
    
1053
    /**
1054
     * Allow overrides to add alter variables before entity assign & save
1055
     * @param ModelInterface $entity
1056
     * @param array $post
1057
     * @param array|null $whiteList
1058
     * @param array|null $columnMap
1059
     * @return void
1060
     */
1061
    protected function beforeAssign(ModelInterface &$entity, Array &$post, ?Array &$whiteList, ?Array &$columnMap): void {
1062
    
1063
    }
0 ignored issues
show
Coding Style introduced by
Function closing brace must go on the next line following the body; found 1 blank lines before brace
Loading history...
1064
    
1065
    /**
1066
     * @param $single
1067
     *
1068
     * @return void
1069
     */
1070
    protected function saveEntity($entity): array
1071
    {
1072
        $ret = [];
1073
        $ret['saved'] = $entity->save();
1074
        $ret['messages'] = $entity->getMessages();
1075
        $ret['model'] = get_class($entity);
1076
        $ret['source'] = $entity->getSource();
1077
        $ret['entity'] = $entity->expose($this->getExpose());
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getExpose() targeting Zemit\Mvc\Controller\Model::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...
1078
        return $ret;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $ret returns the type array which is incompatible with the documented return type void.
Loading history...
1079
    }
1080
    
1081
    /**
1082
     * Try to find the appropriate model from the current controller name
1083
     *
1084
     * @param ?string $controllerName
1085
     * @param ?array $namespaces
1086
     * @param string $needle
1087
     *
1088
     * @return string|null
1089
     */
1090
    public function getModelNameFromController(string $controllerName = null, array $namespaces = null, string $needle = 'Models'): ?string
1091
    {
1092
        $controllerName ??= $this->dispatcher->getControllerName() ?? '';
1093
        $namespaces ??= $this->loader->getNamespaces() ?? [];
1094
        
1095
        $model = ucfirst(Text::camelize(Text::uncamelize($controllerName)));
1096
        if (!class_exists($model)) {
1097
            foreach ($namespaces as $namespace => $path) {
1098
                $possibleModel = $namespace . '\\' . $model;
1099
                if (strpos($namespace, $needle) !== false && class_exists($possibleModel)) {
1100
                    $model = $possibleModel;
1101
                }
1102
            }
1103
        }
1104
        
1105
        return class_exists($model) && new $model() instanceof ModelInterface ? $model : null;
1106
    }
1107
    
1108
    /**
1109
     * Get message from list of entities
1110
     *
1111
     * @param $list Resultset|\Phalcon\Mvc\Model
1112
     *
1113
     * @return array|bool
1114
     * @deprecated
1115
     *
1116
     */
1117
    public function getRestMessages($list = null)
1118
    {
1119
        if (!is_array($list)) {
1120
            $list = [$list];
1121
        }
1122
        
1123
        $ret = [];
1124
        
1125
        foreach ($list as $single) {
1126
            
1127
            if ($single) {
1128
                
1129
                /** @var Messages $validations */
1130
                $messages = $single instanceof Message ? $list : $single->getMessages();
1131
                
1132
                if ($messages && (is_array($messages) || $messages instanceof \Traversable)) {
1133
                    
1134
                    foreach ($messages as $message) {
1135
                        
1136
                        $validationFields = $message->getField();
1137
                        
1138
                        if (!is_array($validationFields)) {
1139
                            $validationFields = [$validationFields];
1140
                        }
1141
                        
1142
                        foreach ($validationFields as $validationField) {
1143
                            
1144
                            if (empty($ret[$validationField])) {
1145
                                $ret[$validationField] = [];
1146
                            }
1147
                            
1148
                            $ret[$validationField][] = [
1149
                                'field' => $message->getField(),
1150
                                'code' => $message->getCode(),
1151
                                'type' => $message->getType(),
1152
                                'message' => $message->getMessage(),
1153
                                'metaData' => $message->getMetaData(),
1154
                            ];
1155
                        }
1156
                    }
1157
                }
1158
            }
1159
        }
1160
        
1161
        return $ret ?: false;
1162
    }
1163
}
1164