Test Failed
Push — master ( 656c0f...c8313f )
by Julien
15:26
created

Model::getModelClassName()   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 0
Metric Value
eloc 1
c 0
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|\Zemit\Mvc\Model
56
     */
57
    public function getModelClassName()
58
    {
59
        return $this->getModelNameFromController();
60
    }
61
    
62
    /**
63
     * Get the WhiteList parameters for saving
64
     * @todo add a whitelist object that would be able to support one configuration for the search, assign, filter
65
     *
66
     * @return null|array
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 relationship eager loading definition for a listing
139
     *
140
     * @return null|array
141
     */
142
    protected function getExportWith()
143
    {
144
        return $this->getListWith();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getListWith() targeting Zemit\Mvc\Controller\Model::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...
145
    }
146
    
147
    /**
148
     * Get expose definition for a single entity
149
     *
150
     * @return null|array
151
     */
152
    protected function getExpose()
153
    {
154
        return null;
155
    }
156
    
157
    /**
158
     * Get expose definition for listing many entities
159
     *
160
     * @return null|array
161
     */
162
    protected function getListExpose()
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 expose definition for export
169
     *
170
     * @return null|array
171
     */
172
    protected function getExportExpose()
173
    {
174
        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...
175
    }
176
    
177
    /**
178
     * Get columns merge definition for export
179
     *
180
     * @return null|array
181
     */
182
    public function getExportMergeColum()
183
    {
184
        return null;
185
    }
186
    
187
    /**
188
     * Get columns format field text definition for export
189
     *
190
     * @param array|null $params
191
     *
192
     * @return null|array
193
     */
194
    public function getExportFormatFieldText(?array $params = null)
195
    {
196
        return null;
197
    }
198
    
199
    /**
200
     * Get join definition
201
     *
202
     * @return null|array
203
     */
204
    protected function getJoins()
205
    {
206
        return null;
207
    }
208
    
209
    /**
210
     * Get the order definition
211
     *
212
     * @return null|array
213
     */
214
    protected function getOrder()
215
    {
216
        return $this->getParamExplodeArrayMapFilter('order');
217
    }
218
    
219
    /**
220
     * Get the current limit value
221
     *
222
     * @return null|int Default: 1000
223
     */
224
    protected function getLimit(): int
225
    {
226
        return (int)$this->getParam('limit', 'int', 1000);
227
    }
228
    
229
    /**
230
     * Get the current offset value
231
     *
232
     * @return null|int Default: 0
233
     */
234
    protected function getOffset(): int
235
    {
236
        return (int)$this->getParam('offset', 'int', 0);
237
    }
238
    
239
    /**
240
     * Get group
241
     * - Automatically group by ID by default if nothing else is provided
242
     * - This will fix multiple single records being returned for the same model with joins
243
     *
244
     * @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...
245
     */
246
    protected function getGroup()
247
    {
248
        $group = $this->getParamExplodeArrayMapFilter('group');
249
        
250
        // Fix for joins, automatically append grouping if none provided
251
        $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...
252
        if (empty($group) && !empty($join)) {
253
            $group = $this->appendModelName('id');
254
        }
255
        
256
        return $group;
257
    }
258
    
259
    /**
260
     * Get distinct
261
     * @TODO see how to implement this, maybe an action itself
262
     *
263
     * @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...
264
     */
265
    protected function getDistinct()
266
    {
267
        return $this->getParamExplodeArrayMapFilter('distinct');
268
    }
269
    
270
    /**
271
     * Get columns
272
     * @TODO see how to implement this
273
     *
274
     * @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...
275
     */
276
    protected function getColumns()
277
    {
278
        return $this->getParam('columns', 'string');
279
    }
280
    
281
    /**
282
     * Return the whitelisted role list for the current model
283
     *
284
     * @return string[] By default will return dev and admin role
285
     */
286
    protected function getRoleList()
287
    {
288
        return ['dev', 'admin'];
289
    }
290
    
291
    /**
292
     * Get Search condition
293
     *
294
     * @return string Default: deleted = 0
295
     */
296
    protected function getSearchCondition()
297
    {
298
        $conditions = [];
299
        
300
        $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

300
        $searchList = array_values(array_filter(array_unique(explode(' ', /** @scrutinizer ignore-type */ $this->getParam('search', 'string')))));
Loading history...
301
        
302
        foreach ($searchList as $searchTerm) {
303
            $orConditions = [];
304
            $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...
305
            if ($searchWhiteList) {
306
                foreach ($searchWhiteList as $whiteList) {
0 ignored issues
show
Bug introduced by
The expression $searchWhiteList of type void is not traversable.
Loading history...
307
                    
308
                    // Multidimensional arrays not supported yet
309
                    // @todo support this properly
310
                    if (is_array($whiteList)) {
311
                        continue;
312
                    }
313
                    
314
                    $searchTermBinding = '_' . uniqid() . '_';
315
                    $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

315
                    $orConditions [] = /** @scrutinizer ignore-type */ $this->appendModelName($whiteList) . " like :$searchTermBinding:";
Loading history...
316
                    $this->setBind([$searchTermBinding => '%' . $searchTerm . '%']);
317
                    $this->setBindTypes([$searchTermBinding => Column::BIND_PARAM_STR]);
318
                }
319
            }
320
            
321
            if (!empty($orConditions)) {
322
                $conditions [] = '(' . implode(' or ', $orConditions) . ')';
323
            }
324
        }
325
        
326
        return empty($conditions) ? null : '(' . implode(' and ', $conditions) . ')';
327
    }
328
    
329
    /**
330
     * Get Soft delete condition
331
     *
332
     * @return string Default: deleted = 0
333
     */
334
    protected function getSoftDeleteCondition(): ?string
335
    {
336
        return '[' . $this->getModelClassName() . '].[deleted] = 0';
337
    }
338
    
339
    /**
340
     * @param $field
341
     * @param string $sanitizer
342
     * @param string $glue
343
     *
344
     * @return array|string[]
345
     */
346
    public function getParamExplodeArrayMapFilter($field, $sanitizer = 'string', $glue = ',')
347
    {
348
        $filter = $this->filter;
349
        $ret = array_filter(array_map(function ($e) use ($filter, $sanitizer) {
350
            
351
            // allow to run RAND()
352
            if (strrpos($e, 'RAND()') === 0) {
353
                return $e;
354
            }
355
            
356
            return $this->appendModelName(trim($filter->sanitize($e, $sanitizer)));
357
        }, 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

357
        }, explode($glue, /** @scrutinizer ignore-type */ $this->getParam($field, $sanitizer))));
Loading history...
358
        
359
        return empty($ret) ? null : $ret;
360
    }
361
    
362
    /**
363
     * Set the variables to bind
364
     *
365
     * @param array $bind Variable bind to merge or replace
366
     * @param bool $replace Pass true to replace the entire bind set
367
     */
368
    public function setBind(array $bind = [], bool $replace = false)
369
    {
370
        $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

370
        $this->_bind = $replace ? $bind : array_merge(/** @scrutinizer ignore-type */ $this->getBind(), $bind);
Loading history...
371
    }
372
    
373
    /**
374
     * Get the current bind
375
     * key => value
376
     *
377
     * @return array|null
378
     */
379
    public function getBind()
380
    {
381
        return $this->_bind ?? null;
382
    }
383
    
384
    /**
385
     * Set the variables types to bind
386
     *
387
     * @param array $bindTypes Variable bind types to merge or replace
388
     * @param bool $replace Pass true to replace the entire bind type set
389
     */
390
    public function setBindTypes(array $bindTypes = [], bool $replace = false)
391
    {
392
        $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

392
        $this->_bindTypes = $replace ? $bindTypes : array_merge(/** @scrutinizer ignore-type */ $this->getBindTypes(), $bindTypes);
Loading history...
393
    }
394
    
395
    /**
396
     * Get the current bind types
397
     *
398
     * @return array|null
399
     */
400
    public function getBindTypes()
401
    {
402
        return $this->_bindTypes ?? null;
403
    }
404
    
405
    /**
406
     * Get Created By Condition
407
     *
408
     * @param string[] $columns
409
     * @param Identity|null $identity
410
     * @param string[]|null $roleList
411
     *
412
     * @return null
413
     *
414
     * @return string|null
415
     */
416
    protected function getIdentityCondition(array $columns = null, Identity $identity = null, $roleList = null)
417
    {
418
        $identity ??= $this->identity ?? false;
419
        $roleList ??= $this->getRoleList();
420
        $modelName = $this->getModelClassName();
421
        
422
        if ($modelName && $identity && !$identity->hasRole($roleList)) {
423
            $ret = [];
424
            
425
            $columns ??= [
426
                'createdBy',
427
                'ownedBy',
428
                'userId',
429
            ];
430
            
431
            foreach ($columns as $column) {
432
                if (!property_exists($modelName, $column)) {
433
                    continue;
434
                }
435
                
436
                $field = strpos($column, '.') !== false ? $column : $modelName . '.' . $column;
437
                $field = '[' . str_replace('.', '].[', $field) . ']';
438
                
439
                $this->setBind([$column => (int)$identity->getUserId()]);
440
                $this->setBindTypes([$column => Column::BIND_PARAM_INT]);
441
                $ret [] = $field . ' = :' . $column . ':';
442
            }
443
            
444
            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...
445
        }
446
        
447
        return null;
448
    }
449
    
450
    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...
451
    {
452
        $func = function ($item) use (&$func, &$callback) {
453
            return is_array($item) ? array_map($func, $item) : call_user_func($callback, $item);
454
        };
455
        
456
        return array_map($func, $array);
457
    }
458
    
459
    /**
460
     * Get Filter Condition
461
     *
462
     * @param array|null $filters
463
     * @param array|null $whiteList
464
     * @param bool $or
465
     *
466
     * @return string|null Return the generated query
467
     * @throws \Exception Throw an exception if the field property is not valid
468
     * @todo escape fields properly
469
     *
470
     */
471
    protected function getFilterCondition(array $filters = null, array $whiteList = null, $or = false)
472
    {
473
        $filters ??= $this->getParam('filters');
474
        $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...
475
        $whiteList = $this->getFlatWhiteList($whiteList);
476
        $lowercaseWhiteList = !is_null($whiteList) ? $this->arrayMapRecursive('mb_strtolower', $whiteList) : $whiteList;
477
        
478
        // No filter, no query
479
        if (empty($filters)) {
480
            return null;
481
        }
482
        
483
        $query = [];
484
        foreach ($filters as $filter) {
485
            $field = $this->filter->sanitize($filter['field'] ?? null, ['string', 'trim']);
486
            
487
            if (!empty($field)) {
488
                $lowercaseField = mb_strtolower($field);
489
                
490
                // whiteList on filter condition
491
                if (is_null($whiteList) || !in_array($lowercaseField, $lowercaseWhiteList ?? [], true)) {
492
                    // @todo if config is set to throw exception on usage of not allowed filters otherwise continue looping through
493
                    throw new \Exception('Not allowed to filter using the following field: `' . $field . '`', 403);
494
                }
495
                
496
                $uniqid = substr(md5(json_encode($filter)), 0, 10);
497
//                $queryField = '_' . uniqid($uniqid . '_field_') . '_';
498
                $queryValue = '_' . uniqid($uniqid . '_value_') . '_';
499
                $queryOperator = strtolower($filter['operator']);
500
    
501
                // Map alias query operator
502
                $mapAlias = [
503
                    'equals' => '=',
504
                    'not equal' => '!=',
505
                    'does not equal' => '!=',
506
                    'different than' => '<>',
507
                    'greater than' => '>',
508
                    'greater than or equal' => '>=',
509
                    'less than' => '<',
510
                    'less than or equal' => '<=',
511
                    'null-safe equal' => '<=>',
512
                ];
513
                $queryOperator = $mapAlias[$queryOperator] ?? $queryOperator;
514
                
515
                switch ($queryOperator) {
516
                    
517
                    // mysql native
518
                    case '=': // Equal operator
519
                    case '!=': // Not equal operator
520
                    case '<>': // Not equal operator
521
                    case '>': // Greater than operator
522
                    case '>=': // Greater than or equal operator
523
                    case '<': // Less than or equal operator
524
                    case '<=': // Less than or equal operator
525
                    case '<=>': // NULL-safe equal to operator
526
                    case 'in': // Whether a value is within a set of values
527
                    case 'not in': // Whether a value is not within a set of values
528
                    case 'like': // Simple pattern matching
529
                    case 'not like': // Negation of simple pattern matching
530
                    case 'between': // Whether a value is within a range of values
531
                    case 'not between': // Whether a value is not within a range of values
532
                    case 'is': // Test a value against a boolean
533
                    case 'is not': // Test a value against a boolean
534
                    case 'is null': // NULL value test
535
                    case 'is not null': // NOT NULL value test
536
                    case 'is false': // Test a value against a boolean
537
                    case 'is not false': // // Test a value against a boolean
538
                    case 'is true': // // Test a value against a boolean
539
                    case 'is not true': // // Test a value against a boolean
540
                        break;
541
                        
542
                    // advanced filters
543
                    case 'regexp':
544
                    case 'not regexp':
545
                    case 'contains':
546
                    case 'does not contain':
547
                    case 'contains word':
548
                    case 'does not contain word':
549
                    case 'distance sphere greater than':
550
                    case 'distance sphere greater than or equal':
551
                    case 'distance sphere less than':
552
                    case 'distance sphere less than or equal':
553
                    case 'is empty':
554
                    case 'is not empty':
555
                        break;
556
                        
557
                    default:
558
                        throw new \Exception('Not allowed to filter using the following operator: `' . $queryOperator . '`', 403);
559
                }
560
                
561
                $bind = [];
562
                $bindType = [];
563
564
//                $bind[$queryField] = $filter['field'];
565
//                $bindType[$queryField] = Column::BIND_PARAM_STR;
566
//                $queryFieldBinder = ':' . $queryField . ':';
567
//                $queryFieldBinder = '{' . $queryField . '}';
568
                
569
                // Add the current model name by default
570
                $field = $this->appendModelName($field);
571
                
572
                $queryFieldBinder = $field;
573
                $queryValueBinder = ':' . $queryValue . ':';
574
                if (isset($filter['value'])) {
575
                    // special for between and not between
576
                    if (in_array($queryOperator, ['between', 'not between'])) {
577
                        $queryValue0 = '_' . uniqid($uniqid . '_value_') . '_';
578
                        $queryValue1 = '_' . uniqid($uniqid . '_value_') . '_';
579
                        $bind[$queryValue0] = $filter['value'][0];
580
                        $bind[$queryValue1] = $filter['value'][1];
581
                        $bindType[$queryValue0] = Column::BIND_PARAM_STR;
582
                        $bindType[$queryValue1] = Column::BIND_PARAM_STR;
583
                        $query [] = (($queryOperator === 'not between') ? 'not ' : null) . "$queryFieldBinder between :$queryValue0: and :$queryValue1:";
584
                    }
585
                    
586
                    else if (in_array($queryOperator, ['contains', 'does not contain'])) {
587
                        $queryValue0 = '_' . uniqid($uniqid . '_value_') . '_';
588
                        $queryValue1 = '_' . uniqid($uniqid . '_value_') . '_';
589
                        $queryValue2 = '_' . uniqid($uniqid . '_value_') . '_';
590
                        $bind[$queryValue0] = '%' . $filter['value'] . '%';
591
                        $bind[$queryValue1] = '%' . $filter['value'];
592
                        $bind[$queryValue2] = $filter['value'] . '%';
593
                        $bindType[$queryValue0] = Column::BIND_PARAM_STR;
594
                        $bindType[$queryValue1] = Column::BIND_PARAM_STR;
595
                        $bindType[$queryValue2] = Column::BIND_PARAM_STR;
596
                        $query [] = ($queryOperator === 'does not contain'? '!' : '') . "($queryFieldBinder like :$queryValue0: or $queryFieldBinder like :$queryValue1: or $queryFieldBinder like :$queryValue2:)";
597
                    }
598
                    
599
                    else if (in_array($queryOperator, ['is empty', 'is not empty'])) {
600
                        $query [] = ($queryOperator === 'is not empty'? '!' : '') . "(TRIM($queryFieldBinder) = '' or $queryFieldBinder is null)";
601
                    }
602
                    
603
                    else if (in_array($queryOperator, ['regexp', 'not regexp'])) {
604
                        $bind[$queryValue] = $filter['value'];
605
                        $query [] = $queryOperator . "($queryFieldBinder, $queryValueBinder)";
606
                    }
607
                    
608
                    else if (in_array($queryOperator, ['contains word', 'does not contain word'])) {
609
                        $bind[$queryValue] = '\\b' . $filter['value'] . '\\b';
610
                        $regexQueryOperator = str_replace(['contains word', 'does not contain word'], ['regexp', 'not regexp'], $queryOperator);
611
                        $query [] = $regexQueryOperator . "($queryFieldBinder, $queryValueBinder)";
612
                    }
613
                    
614
                    else if (in_array($queryOperator, [
615
                        'distance sphere equals',
616
                        'distance sphere greater than',
617
                        'distance sphere greater than or equal',
618
                        'distance sphere less than',
619
                        'distance sphere less than or equal',
620
                    ])) {
621
                        // Prepare values binding of 2 sphere point to calculate distance
622
                        $queryBindValue0 = '_' . uniqid($uniqid . '_value_') . '_';
623
                        $queryBindValue1 = '_' . uniqid($uniqid . '_value_') . '_';
624
                        $queryBindValue2 = '_' . uniqid($uniqid . '_value_') . '_';
625
                        $queryBindValue3 = '_' . uniqid($uniqid . '_value_') . '_';
626
                        $bind[$queryBindValue0] = $filter['value'][0];
627
                        $bind[$queryBindValue1] = $filter['value'][1];
628
                        $bind[$queryBindValue2] = $filter['value'][2];
629
                        $bind[$queryBindValue3] = $filter['value'][3];
630
                        $bindType[$queryBindValue0] = Column::BIND_PARAM_DECIMAL;
631
                        $bindType[$queryBindValue1] = Column::BIND_PARAM_DECIMAL;
632
                        $bindType[$queryBindValue2] = Column::BIND_PARAM_DECIMAL;
633
                        $bindType[$queryBindValue3] = Column::BIND_PARAM_DECIMAL;
634
    
635
                        $queryPointLatBinder0 = $queryBindValue0;
636
                        $queryPointLonBinder0 = $queryBindValue1;
637
                        $queryPointLatBinder1 = $queryBindValue2;
638
                        $queryPointLonBinder1 = $queryBindValue3;
639
    
640
                        $queryLogicalOperator =
641
                            (strpos($queryOperator, 'greater') !== false? '>' : null) .
642
                            (strpos($queryOperator, 'less') !== false? '<' : null) .
643
                            (strpos($queryOperator, 'equal') !== false? '=' : null);
644
    
645
                        $bind[$queryValue] = $filter['value'];
646
                        $query [] = "ST_Distance_Sphere(point($queryPointLatBinder0, $queryPointLonBinder0), point($queryPointLatBinder1, $queryPointLonBinder1)) $queryLogicalOperator $queryValueBinder";
647
                    }
648
                    
649
                    else {
650
                        $bind[$queryValue] = $filter['value'];
651
                        if (is_string($filter['value'])) {
652
                            $bindType[$queryValue] = Column::BIND_PARAM_STR;
653
                        }
654
                        else if (is_int($filter['value'])) {
655
                            $bindType[$queryValue] = Column::BIND_PARAM_INT;
656
                        }
657
                        else if (is_bool($filter['value'])) {
658
                            $bindType[$queryValue] = Column::BIND_PARAM_BOOL;
659
                        }
660
                        else if (is_float($filter['value'])) {
661
                            $bindType[$queryValue] = Column::BIND_PARAM_DECIMAL;
662
                        }
663
                        else if (is_double($filter['value'])) {
664
                            $bindType[$queryValue] = Column::BIND_PARAM_DECIMAL;
665
                        }
666
                        else if (is_array($filter['value'])) {
667
                            $queryValueBinder = '({' . $queryValue . ':array})';
668
                            $bindType[$queryValue] = Column::BIND_PARAM_STR;
669
                        }
670
                        else {
671
                            $bindType[$queryValue] = Column::BIND_PARAM_NULL;
672
                        }
673
                        $query [] = "$queryFieldBinder $queryOperator $queryValueBinder";
674
                    }
675
                }
676
                else {
677
                    $query [] = "$queryFieldBinder $queryOperator";
678
                }
679
                
680
                $this->setBind($bind);
681
                $this->setBindTypes($bindType);
682
            }
683
            else {
684
                if (is_array($filter)) {
685
                    $query [] = $this->getFilterCondition($filter, $whiteList, !$or);
686
                }
687
                else {
688
                    throw new \Exception('A valid field property is required.', 400);
689
                }
690
            }
691
        }
692
        
693
        return empty($query) ? null : '(' . implode($or ? ' or ' : ' and ', $query) . ')';
694
    }
695
    
696
    /**
697
     * Append the current model name alias to the field
698
     * So: field -> [Alias].[field]
699
     *
700
     * @param string|array $field
701
     * @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...
702
     *
703
     * @return array|string
704
     */
705
    public function appendModelName($field, $modelName = null)
706
    {
707
        $modelName ??= $this->getModelClassName();
708
        
709
        if (empty($field)) {
710
            return $field;
711
        }
712
        
713
        if (is_string($field)) {
714
            // Add the current model name by default
715
            $explode = explode(' ', $field);
716
            if (!strpos($field, '.') !== false) {
717
                $field = trim('[' . $modelName . '].[' . array_shift($explode) . '] ' . implode(' ', $explode));
718
            }
719
            else if (strpos($field, ']') === false && strpos($field, '[') === false) {
720
                $field = trim('[' . implode('].[', explode('.', array_shift($explode))) . ']' . implode(' ', $explode));
721
            }
722
        }
723
        else if (is_array($field)) {
0 ignored issues
show
introduced by
The condition is_array($field) is always true.
Loading history...
724
            foreach ($field as $fieldKey => $fieldValue) {
725
                $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

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

998
        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...
999
    }
1000
    
1001
    /**
1002
     * Saving model automagically
1003
     *
1004
     * Note:
1005
     * If a newly created entity can't be retrieved using the ->getSingle
1006
     * method after it's creation, the entity will be returned directly
1007
     *
1008
     * @TODO Support Composite Primary Key
1009
     *
1010
     * @param null|int|string $id
1011
     * @param null|\Zemit\Mvc\Model $entity
1012
     * @param null|mixed $post
1013
     * @param null|string $modelName
1014
     * @param null|array $whiteList
1015
     * @param null|array $columnMap
1016
     * @param null|array $with
1017
     *
1018
     * @return array
1019
     */
1020
    protected function save($id = null, $entity = null, $post = null, $modelName = null, $whiteList = null, $columnMap = null, $with = null)
1021
    {
1022
        $single = false;
1023
        $retList = [];
1024
        
1025
        // Get the model name to play with
1026
        $modelName ??= $this->getModelClassName();
1027
        $post ??= $this->getParams();
1028
        $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...
1029
        $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...
1030
        $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...
1031
        $id = (int)$id;
1032
        
1033
        // Check if multi-d post
1034
        if (!empty($id) || !isset($post[0]) || !is_array($post[0])) {
1035
            $single = true;
1036
            $post = [$post];
1037
        }
1038
        
1039
        // Save each posts
1040
        foreach ($post as $key => $singlePost) {
1041
            $ret = [];
1042
            
1043
            $singlePostId = (!$single || empty($id)) ? $this->getParam('id', 'int', $this->getParam('int', 'int', $singlePost['id'] ?? null)) : $id;
0 ignored issues
show
Bug introduced by
It seems like $this->getParam('int', '...nglePost['id'] ?? 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

1043
            $singlePostId = (!$single || empty($id)) ? $this->getParam('id', 'int', /** @scrutinizer ignore-type */ $this->getParam('int', 'int', $singlePost['id'] ?? null)) : $id;
Loading history...
1044
            if (isset($singlePost['id'])) {
1045
                unset($singlePost['id']);
1046
            }
1047
            
1048
            /** @var \Zemit\Mvc\Model $singlePostEntity */
1049
            $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

1049
            $singlePostEntity = (!$single || !isset($entity)) ? $this->getSingle(/** @scrutinizer ignore-type */ $singlePostId, $modelName) : $entity;
Loading history...
1050
            
1051
            // Create entity if not exists
1052
            if (!$singlePostEntity && empty($singlePostId)) {
1053
                $singlePostEntity = new $modelName();
1054
            }
1055
            
1056
            if (!$singlePostEntity) {
1057
                $ret = [
1058
                    'saved' => false,
1059
                    'messages' => [new Message('Entity id `' . $singlePostId . '` not found.', $modelName, 'NotFound', 404)],
1060
                    'model' => $modelName,
1061
                    'source' => (new $modelName)->getSource(),
1062
                ];
1063
            }
1064
            else {
1065
                // allow custom manipulations
1066
                // @todo move this using events
1067
                $this->beforeAssign($singlePostEntity, $singlePost, $whiteList, $columnMap);
1068
                
1069
                // assign & save
1070
                $singlePostEntity->assign($singlePost, $whiteList, $columnMap);
1071
                $ret = $this->saveEntity($singlePostEntity);
1072
                
1073
                // refetch & expose
1074
                $fetch = $this->getSingle($singlePostEntity->getId(), $modelName, $with);
1075
                $ret['single'] = $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

1075
                $ret['single'] = $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...
1076
            }
1077
            
1078
            $retList [] = $ret;
1079
        }
1080
        
1081
        return $single ? $retList[0] : $retList;
1082
    }
1083
    
1084
    /**
1085
     * Allow overrides to add alter variables before entity assign & save
1086
     * @param ModelInterface $entity
1087
     * @param array $post
1088
     * @param array|null $whiteList
1089
     * @param array|null $columnMap
1090
     * @return void
1091
     */
1092
    protected function beforeAssign(ModelInterface &$entity, Array &$post, ?Array &$whiteList, ?Array &$columnMap): void {
1093
    
1094
    }
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...
1095
    
1096
    /**
1097
     * @param $single
1098
     *
1099
     * @return void
1100
     */
1101
    protected function saveEntity($entity): array
1102
    {
1103
        $ret = [];
1104
        $ret['saved'] = $entity->save();
1105
        $ret['messages'] = $entity->getMessages();
1106
        $ret['model'] = get_class($entity);
1107
        $ret['source'] = $entity->getSource();
1108
        $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...
1109
        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...
1110
    }
1111
    
1112
    /**
1113
     * Try to find the appropriate model from the current controller name
1114
     *
1115
     * @param ?string $controllerName
1116
     * @param ?array $namespaces
1117
     * @param string $needle
1118
     *
1119
     * @return string|null|\Zemit\Mvc\Model
1120
     */
1121
    public function getModelNameFromController(string $controllerName = null, array $namespaces = null, string $needle = 'Models'): ?string
1122
    {
1123
        $controllerName ??= $this->dispatcher->getControllerName() ?? '';
1124
        $namespaces ??= $this->loader->getNamespaces() ?? [];
1125
        
1126
        $model = ucfirst(Text::camelize(Text::uncamelize($controllerName)));
1127
        if (!class_exists($model)) {
1128
            foreach ($namespaces as $namespace => $path) {
1129
                $possibleModel = $namespace . '\\' . $model;
1130
                if (strpos($namespace, $needle) !== false && class_exists($possibleModel)) {
1131
                    $model = $possibleModel;
1132
                }
1133
            }
1134
        }
1135
        
1136
        return class_exists($model) && new $model() instanceof ModelInterface ? $model : null;
1137
    }
1138
    
1139
    /**
1140
     * Get message from list of entities
1141
     *
1142
     * @param $list Resultset|\Phalcon\Mvc\Model
1143
     *
1144
     * @return array|bool
1145
     * @deprecated
1146
     *
1147
     */
1148
    public function getRestMessages($list = null)
1149
    {
1150
        if (!is_array($list)) {
1151
            $list = [$list];
1152
        }
1153
        
1154
        $ret = [];
1155
        
1156
        foreach ($list as $single) {
1157
            
1158
            if ($single) {
1159
                
1160
                /** @var Messages $validations */
1161
                $messages = $single instanceof Message ? $list : $single->getMessages();
1162
                
1163
                if ($messages && (is_array($messages) || $messages instanceof \Traversable)) {
1164
                    
1165
                    foreach ($messages as $message) {
1166
                        
1167
                        $validationFields = $message->getField();
1168
                        
1169
                        if (!is_array($validationFields)) {
1170
                            $validationFields = [$validationFields];
1171
                        }
1172
                        
1173
                        foreach ($validationFields as $validationField) {
1174
                            
1175
                            if (empty($ret[$validationField])) {
1176
                                $ret[$validationField] = [];
1177
                            }
1178
                            
1179
                            $ret[$validationField][] = [
1180
                                'field' => $message->getField(),
1181
                                'code' => $message->getCode(),
1182
                                'type' => $message->getType(),
1183
                                'message' => $message->getMessage(),
1184
                                'metaData' => $message->getMetaData(),
1185
                            ];
1186
                        }
1187
                    }
1188
                }
1189
            }
1190
        }
1191
        
1192
        return $ret ?: false;
1193
    }
1194
}
1195