Passed
Push — master ( 001f87...7a9f93 )
by Julien
05:09
created

Model::appendModelName()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 9
c 2
b 0
f 0
dl 0
loc 18
ccs 0
cts 10
cp 0
rs 9.6111
cc 5
nc 4
nop 2
crap 30
1
<?php
2
3
/**
4
 * This file is part of the Zemit Framework.
5
 *
6
 * (c) Zemit Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Zemit\Mvc\Controller;
13
14
use Phalcon\Db\Column;
15
use Phalcon\Messages\Message;
16
use Phalcon\Messages\Messages;
17
use Phalcon\Mvc\Model\Resultset;
18
use Phalcon\Mvc\ModelInterface;
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\Support\Exposer\Exposer;
22
use Zemit\Support\Helper;
23
use Zemit\Support\Slug;
24
25
/**
26
 * Trait Model
27
 */
28
trait Model
29
{
30
    use Params;
31
    
32
    protected $_bind = [];
33
    protected $_bindTypes = [];
34
    
35
    /**
36
     * Get the current Model Name
37
     * @return string|null
38
     * @todo remove for v1
39
     *
40
     * @deprecated change to getModelClassName() instead
41
     */
42
    public function getModelName()
43
    {
44
        return $this->getModelClassName();
45
    }
46
    
47
    /**
48
     * Get the current Model Class Name
49
     *
50
     * @return string|null|\Zemit\Mvc\Model
51
     */
52
    public function getModelClassName()
53
    {
54
        return $this->getModelNameFromController();
55
    }
56
    
57
    /**
58
     * Get the WhiteList parameters for saving
59
     * @return null|array
60
     * @todo add a whitelist object that would be able to support one configuration for the search, assign, filter
61
     *
62
     */
63
    protected function getWhiteList()
64
    {
65
        return null;
66
    }
67
    
68
    /**
69
     * Get the Flattened WhiteList
70
     *
71
     * @param array|null $whiteList
72
     *
73
     * @return array|null
74
     */
75
    public function getFlatWhiteList(?array $whiteList = null)
76
    {
77
        $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...
78
        $ret = Exposer::parseColumnsRecursive($whiteList);
79
        return $ret ? array_keys($ret) : null;
80
    }
81
    
82
    /**
83
     * Get the WhiteList parameters for filtering
84
     *
85
     * @return null|array
86
     */
87
    protected function getFilterWhiteList()
88
    {
89
        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...
90
    }
91
    
92
    /**
93
     * Get the WhiteList parameters for filtering
94
     *
95
     * @return null|array
96
     */
97
    protected function getSearchWhiteList()
98
    {
99
        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...
100
    }
101
    
102
    /**
103
     * Get the column mapping for crud
104
     *
105
     * @return null|array
106
     */
107
    protected function getColumnMap()
108
    {
109
        return null;
110
    }
111
    
112
    /**
113
     * Get relationship eager loading definition
114
     *
115
     * @return null|array
116
     */
117
    protected function getWith()
118
    {
119
        return null;
120
    }
121
    
122
    /**
123
     * Get relationship eager loading definition for a listing
124
     *
125
     * @return null|array
126
     */
127
    protected function getListWith()
128
    {
129
        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...
130
    }
131
    
132
    /**
133
     * Get relationship eager loading definition for a listing
134
     *
135
     * @return null|array
136
     */
137
    protected function getExportWith()
138
    {
139
        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...
140
    }
141
    
142
    /**
143
     * Get expose definition for a single entity
144
     *
145
     * @return null|array
146
     */
147
    protected function getExpose()
148
    {
149
        return null;
150
    }
151
    
152
    /**
153
     * Get expose definition for listing many entities
154
     *
155
     * @return null|array
156
     */
157
    protected function getListExpose()
158
    {
159
        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...
160
    }
161
    
162
    /**
163
     * Get expose definition for export
164
     *
165
     * @return null|array
166
     */
167
    protected function getExportExpose()
168
    {
169
        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...
170
    }
171
    
172
    /**
173
     * Get columns merge definition for export
174
     *
175
     * @return null|array
176
     */
177
    public function getExportMergeColum()
178
    {
179
        return null;
180
    }
181
    
182
    /**
183
     * Get columns format field text definition for export
184
     *
185
     * @param array|null $params
186
     *
187
     * @return null|array
188
     */
189
    public function getExportFormatFieldText(?array $params = null)
190
    {
191
        return null;
192
    }
193
    
194
    /**
195
     * Get join definition
196
     *
197
     * @return null|array
198
     */
199
    protected function getJoins()
200
    {
201
        return null;
202
    }
203
    
204
    /**
205
     * Get the order definition
206
     *
207
     * @return null|array
208
     */
209
    protected function getOrder()
210
    {
211
        return $this->getParamExplodeArrayMapFilter('order');
212
    }
213
    
214
    /**
215
     * Get the current limit value
216
     *
217
     * @return null|int Default: 1000
218
     */
219
    protected function getLimit(): ?int
220
    {
221
        $limit = (int)$this->getParam('limit', 'int', 1000);
222
        return $limit === -1? null : (int)abs($limit);
223
    }
224
    
225
    /**
226
     * Get the current offset value
227
     *
228
     * @return null|int Default: 0
229
     */
230
    protected function getOffset(): int
231
    {
232
        return (int)$this->getParam('offset', 'int', 0);
233
    }
234
    
235
    /**
236
     * Get group
237
     * - Automatically group by ID by default if nothing else is provided
238
     * - This will fix multiple single records being returned for the same model with joins
239
     *
240
     * @return array|string|null
241
     */
242
    protected function getGroup()
243
    {
244
        $group = $this->getParamExplodeArrayMapFilter('group');
245
        
246
        // Fix for joins, automatically append grouping if none provided
247
        $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...
248
        if (empty($group) && !empty($join)) {
249
            $group = $this->appendModelName('id');
250
        }
251
        
252
        return $group;
253
    }
254
    
255
    /**
256
     * Get distinct
257
     * @TODO see how to implement this, maybe an action itself
258
     *
259
     * @return array|null
260
     */
261
    protected function getDistinct()
262
    {
263
        return $this->getParamExplodeArrayMapFilter('distinct');
264
    }
265
    
266
    /**
267
     * Get columns
268
     * @TODO see how to implement this
269
     *
270
     * @return array|string|null
271
     */
272
    protected function getColumns()
273
    {
274
        return $this->getParam('columns', 'string');
275
    }
276
    
277
    /**
278
     * Return the whitelisted role list for the current model
279
     *
280
     * @return string[] By default will return dev and admin role
281
     */
282
    protected function getRoleList()
283
    {
284
        return ['dev', 'admin'];
285
    }
286
    
287
    /**
288
     * Get Search condition
289
     *
290
     * @return string Default: deleted = 0
291
     */
292
    protected function getSearchCondition()
293
    {
294
        $conditions = [];
295
        
296
        $searchList = array_values(array_filter(array_unique(explode(' ', $this->getParam('search', 'string') ?? ''))));
297
        
298
        foreach ($searchList as $searchTerm) {
299
            $orConditions = [];
300
            $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...
301
            if ($searchWhiteList) {
302
                foreach ($searchWhiteList as $whiteList) {
0 ignored issues
show
Bug introduced by
The expression $searchWhiteList of type void is not traversable.
Loading history...
303
                    
304
                    // Multidimensional arrays not supported yet
305
                    // @todo support this properly
306
                    if (is_array($whiteList)) {
307
                        continue;
308
                    }
309
                    
310
                    $searchTermBinding = '_' . uniqid() . '_';
311
                    $orConditions [] = $this->appendModelName($whiteList) . " like :$searchTermBinding:";
312
                    $this->setBind([$searchTermBinding => '%' . $searchTerm . '%']);
313
                    $this->setBindTypes([$searchTermBinding => Column::BIND_PARAM_STR]);
314
                }
315
            }
316
            
317
            if (!empty($orConditions)) {
318
                $conditions [] = '(' . implode(' or ', $orConditions) . ')';
319
            }
320
        }
321
        
322
        return empty($conditions) ? null : '(' . implode(' and ', $conditions) . ')';
323
    }
324
    
325
    /**
326
     * Get Soft delete condition
327
     *
328
     * @return string Default: deleted = 0
329
     */
330
    protected function getSoftDeleteCondition(): ?string
331
    {
332
        return '[' . $this->getModelClassName() . '].[deleted] = 0';
333
    }
334
    
335
    /**
336
     * This function will explode the field based using the glue
337
     * and sanitize the values and append the model name
338
     */
339
    public function getParamExplodeArrayMapFilter(string $field, string $sanitizer = 'string', string $glue = ',', int $limit = PHP_INT_MAX) : ?array
340
    {
341
        $ret = [];
342
        $filter = $this->filter;
343
        $params = $this->getParam($field, $sanitizer);
344
        foreach (is_array($params)? $params : [$params] as $param) {
345
            $ret = array_filter(array_merge($ret, array_map(function ($e) use ($filter, $sanitizer) {
346
                return strrpos(strtoupper($e), 'RAND()') === 0? $e : $this->appendModelName(trim($filter->sanitize($e, $sanitizer)));
347
            }, explode($glue, $param ?? '', $limit))));
348
        }
349
        
350
        return empty($ret) ? null : $ret;
351
    }
352
    
353
    /**
354
     * Set the variables to bind
355
     *
356
     * @param array $bind Variable bind to merge or replace
357
     * @param bool $replace Pass true to replace the entire bind set
358
     */
359
    public function setBind(array $bind = [], bool $replace = false)
360
    {
361
        $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

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

383
        $this->_bindTypes = $replace ? $bindTypes : array_merge(/** @scrutinizer ignore-type */ $this->getBindTypes(), $bindTypes);
Loading history...
384
    }
385
    
386
    /**
387
     * Get the current bind types
388
     *
389
     * @return array|null
390
     */
391
    public function getBindTypes()
392
    {
393
        return $this->_bindTypes ?? null;
394
    }
395
    
396
    /**
397
     * Get the identity condition for querying the database: Default created by
398
     *
399
     * @param array|null $columns The columns to check for identity condition
400
     * @param Identity|null $identity The identity object
401
     * @param array|null $roleList The list of roles
402
     *
403
     * @return string|null The generated identity condition or null if no condition is generated
404
     */
405
    protected function getIdentityCondition(?array $columns = null, ?Identity $identity = null, ?array $roleList = null): ?string
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);
434
        }
435
        
436
        return null;
437
    }
438
    
439
    public function arrayMapRecursive(callable $callback, array $array): array
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, bool $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
            // @todo logic bitwise operator
477
//            $logic = $this->filter->sanitize($filter['logic'] ?? null, ['string', 'trim', 'lower']);
478
//            $logic = $logic ?: ($or ? 'or' : 'and');
479
//            $logic = ' ' . $logic . ' ';
480
            
481
            if (!empty($field)) {
482
                $lowercaseField = mb_strtolower($field);
483
                
484
                // whiteList on filter condition
485
                if (is_null($whiteList) || !in_array($lowercaseField, $lowercaseWhiteList ?? [], true)) {
486
                    // @todo if config is set to throw exception on usage of not allowed filters otherwise continue looping through
487
                    throw new \Exception('Not allowed to filter using the following field: `' . $field . '`', 403);
488
                }
489
                
490
                $uniqid = substr(md5(json_encode($filter)), 0, 10);
491
//                $queryField = '_' . uniqid($uniqid . '_field_') . '_';
492
                $queryValue = '_' . uniqid($uniqid . '_value_') . '_';
493
                $queryOperator = strtolower($filter['operator']);
494
                
495
                // Map alias query operator
496
                $mapAlias = [
497
                    'equals' => '=',
498
                    'not equal' => '!=',
499
                    'does not equal' => '!=',
500
                    'different than' => '<>',
501
                    'greater than' => '>',
502
                    'greater than or equal' => '>=',
503
                    'less than' => '<',
504
                    'less than or equal' => '<=',
505
                    'null-safe equal' => '<=>',
506
                ];
507
                $queryOperator = $mapAlias[$queryOperator] ?? $queryOperator;
508
                
509
                switch ($queryOperator) {
510
                    
511
                    // mysql native
512
                    case '=': // Equal operator
513
                    case '!=': // Not equal operator
514
                    case '<>': // Not equal operator
515
                    case '>': // Greater than operator
516
                    case '>=': // Greater than or equal operator
517
                    case '<': // Less than or equal operator
518
                    case '<=': // Less than or equal operator
519
                    case '<=>': // NULL-safe equal to operator
520
                    case 'in': // Whether a value is within a set of values
521
                    case 'not in': // Whether a value is not within a set of values
522
                    case 'like': // Simple pattern matching
523
                    case 'not like': // Negation of simple pattern matching
524
                    case 'between': // Whether a value is within a range of values
525
                    case 'not between': // Whether a value is not within a range of values
526
                    case 'is': // Test a value against a boolean
527
                    case 'is not': // Test a value against a boolean
528
                    case 'is null': // NULL value test
529
                    case 'is not null': // NOT NULL value test
530
                    case 'is false': // Test a value against a boolean
531
                    case 'is not false': // // Test a value against a boolean
532
                    case 'is true': // // Test a value against a boolean
533
                    case 'is not true': // // Test a value against a boolean
534
                        break;
535
                    
536
                    // advanced filters
537
                    case 'start with':
538
                    case 'does not start with':
539
                    case 'end with':
540
                    case 'does not end with':
541
                    case 'regexp':
542
                    case 'not regexp':
543
                    case 'contains':
544
                    case 'does not contain':
545
                    case 'contains word':
546
                    case 'does not contain word':
547
                    case 'distance sphere greater than':
548
                    case 'distance sphere greater than or equal':
549
                    case 'distance sphere less than':
550
                    case 'distance sphere less than or equal':
551
                    case 'is empty':
552
                    case 'is not empty':
553
                        break;
554
                    
555
                    default:
556
                        throw new \Exception('Not allowed to filter using the following operator: `' . $queryOperator . '`', 403);
557
                }
558
                
559
                $bind = [];
560
                $bindType = [];
561
562
//                $bind[$queryField] = $filter['field'];
563
//                $bindType[$queryField] = Column::BIND_PARAM_STR;
564
//                $queryFieldBinder = ':' . $queryField . ':';
565
//                $queryFieldBinder = '{' . $queryField . '}';
566
                
567
                // Add the current model name by default
568
                $field = $this->appendModelName($field);
569
                
570
                $queryFieldBinder = $field;
571
                $queryValueBinder = ':' . $queryValue . ':';
572
                if (isset($filter['value'])) {
573
                    
574
                    
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
                        
580
                        $queryValueIndex = $filter['value'][0] <= $filter['value'][1]? 0 : 1;
581
                        $bind[$queryValue0] = $filter['value'][$queryValueIndex? 1 : 0];
582
                        $bind[$queryValue1] = $filter['value'][$queryValueIndex? 0 : 1];
583
                        
584
                        $bindType[$queryValue0] = Column::BIND_PARAM_STR;
585
                        $bindType[$queryValue1] = Column::BIND_PARAM_STR;
586
                        
587
                        $query [] = (($queryOperator === 'not between') ? 'not ' : null) . "$queryFieldBinder between :$queryValue0: and :$queryValue1:";
588
                    }
589
                    
590
                    elseif (in_array($queryOperator, [
591
                            'distance sphere equals',
592
                            'distance sphere greater than',
593
                            'distance sphere greater than or equal',
594
                            'distance sphere less than',
595
                            'distance sphere less than or equal',
596
                        ])
597
                    ) {
598
                        // Prepare values binding of 2 sphere point to calculate distance
599
                        $queryBindValue0 = '_' . uniqid($uniqid . '_value_') . '_';
600
                        $queryBindValue1 = '_' . uniqid($uniqid . '_value_') . '_';
601
                        $queryBindValue2 = '_' . uniqid($uniqid . '_value_') . '_';
602
                        $queryBindValue3 = '_' . uniqid($uniqid . '_value_') . '_';
603
                        $bind[$queryBindValue0] = $filter['value'][0];
604
                        $bind[$queryBindValue1] = $filter['value'][1];
605
                        $bind[$queryBindValue2] = $filter['value'][2];
606
                        $bind[$queryBindValue3] = $filter['value'][3];
607
                        $bindType[$queryBindValue0] = Column::BIND_PARAM_DECIMAL;
608
                        $bindType[$queryBindValue1] = Column::BIND_PARAM_DECIMAL;
609
                        $bindType[$queryBindValue2] = Column::BIND_PARAM_DECIMAL;
610
                        $bindType[$queryBindValue3] = Column::BIND_PARAM_DECIMAL;
611
                        $queryPointLatBinder0 = ':' . $queryBindValue0 . ':';
612
                        $queryPointLonBinder0 = ':' . $queryBindValue1 . ':';
613
                        $queryPointLatBinder1 = ':' . $queryBindValue2 . ':';
614
                        $queryPointLonBinder1 = ':' . $queryBindValue3 . ':';
615
                        $queryLogicalOperator =
616
                            (strpos($queryOperator, 'greater') !== false ? '>' : null) .
617
                            (strpos($queryOperator, 'less') !== false ? '<' : null) .
618
                            (strpos($queryOperator, 'equal') !== false ? '=' : null);
619
                        
620
                        $bind[$queryValue] = $filter['value'];
621
                        $query [] = "ST_Distance_Sphere(point($queryPointLatBinder0, $queryPointLonBinder0), point($queryPointLatBinder1, $queryPointLonBinder1)) $queryLogicalOperator $queryValueBinder";
622
                    }
623
                    
624
                    elseif (in_array($queryOperator, [
625
                            'in',
626
                            'not in',
627
                        ])
628
                    ) {
629
                        $queryValueBinder = '({' . $queryValue . ':array})';
630
                        $bind[$queryValue] = $filter['value'];
631
                        $bindType[$queryValue] = Column::BIND_PARAM_STR;
632
                        $query [] = "$queryFieldBinder $queryOperator $queryValueBinder";
633
                    }
634
                    
635
                    else {
636
                        $queryAndOr = [];
637
                        
638
                        $valueList = is_array($filter['value']) ? $filter['value'] : [$filter['value']];
639
                        foreach ($valueList as $value) {
640
                            
641
                            $queryValue = '_' . uniqid($uniqid . '_value_') . '_';
642
                            $queryValueBinder = ':' . $queryValue . ':';
643
                            
644
                            if (in_array($queryOperator, ['contains', 'does not contain'])) {
645
                                $queryValue0 = '_' . uniqid($uniqid . '_value_') . '_';
646
                                $queryValue1 = '_' . uniqid($uniqid . '_value_') . '_';
647
                                $queryValue2 = '_' . uniqid($uniqid . '_value_') . '_';
648
                                $bind[$queryValue0] = '%' . $value . '%';
649
                                $bind[$queryValue1] = '%' . $value;
650
                                $bind[$queryValue2] = $value . '%';
651
                                $bindType[$queryValue0] = Column::BIND_PARAM_STR;
652
                                $bindType[$queryValue1] = Column::BIND_PARAM_STR;
653
                                $bindType[$queryValue2] = Column::BIND_PARAM_STR;
654
                                $queryAndOr [] = ($queryOperator === 'does not contain' ? '!' : '') . "($queryFieldBinder like :$queryValue0: or $queryFieldBinder like :$queryValue1: or $queryFieldBinder like :$queryValue2:)";
655
                            }
656
                            
657
                            elseif (in_array($queryOperator, ['starts with', 'does not start with'])) {
658
                                $bind[$queryValue] = $value . '%';
659
                                $bindType[$queryValue] = Column::BIND_PARAM_STR;
660
                                $queryAndOr [] = ($queryOperator === 'does not start with' ? '!' : '') . "($queryFieldBinder like :$queryValue:)";
661
                            }
662
                            
663
                            elseif (in_array($queryOperator, ['ends with', 'does not end with'])) {
664
                                $bind[$queryValue] = '%' . $value;
665
                                $bindType[$queryValue] = Column::BIND_PARAM_STR;
666
                                $queryAndOr [] = ($queryOperator === 'does not end with' ? '!' : '') . "($queryFieldBinder like :$queryValue:)";
667
                            }
668
                            
669
                            elseif (in_array($queryOperator, ['is empty', 'is not empty'])) {
670
                                $queryAndOr [] = ($queryOperator === 'is not empty' ? '!' : '') . "(TRIM($queryFieldBinder) = '' or $queryFieldBinder is null)";
671
                            }
672
                            
673
                            elseif (in_array($queryOperator, ['regexp', 'not regexp'])) {
674
                                $bind[$queryValue] = $value;
675
                                $queryAndOr [] = $queryOperator . "($queryFieldBinder, :$queryValue:)";
676
                            }
677
                            
678
                            elseif (in_array($queryOperator, ['contains word', 'does not contain word'])) {
679
                                $bind[$queryValue] = '\\b' . $value . '\\b';
680
                                $regexQueryOperator = str_replace(['contains word', 'does not contain word'], ['regexp', 'not regexp'], $queryOperator);
681
                                $queryAndOr [] = $regexQueryOperator . "($queryFieldBinder, :$queryValue:)";
682
                            }
683
                            
684
                            else {
685
                                $bind[$queryValue] = $value;
686
                                
687
                                if (is_string($value)) {
688
                                    $bindType[$queryValue] = Column::BIND_PARAM_STR;
689
                                }
690
                                
691
                                elseif (is_int($value)) {
692
                                    $bindType[$queryValue] = Column::BIND_PARAM_INT;
693
                                }
694
                                
695
                                elseif (is_bool($value)) {
696
                                    $bindType[$queryValue] = Column::BIND_PARAM_BOOL;
697
                                }
698
                                
699
                                elseif (is_float($value)) {
700
                                    $bindType[$queryValue] = Column::BIND_PARAM_DECIMAL;
701
                                }
702
                                
703
                                elseif (is_double($value)) {
704
                                    $bindType[$queryValue] = Column::BIND_PARAM_DECIMAL;
705
                                }
706
                                
707
                                elseif (is_array($value)) {
708
                                    $queryValueBinder = '({' . $queryValue . ':array})';
709
                                    $bindType[$queryValue] = Column::BIND_PARAM_STR;
710
                                }
711
                                
712
                                else {
713
                                    $bindType[$queryValue] = Column::BIND_PARAM_NULL;
714
                                }
715
                                
716
                                $queryAndOr [] = "$queryFieldBinder $queryOperator $queryValueBinder";
717
                            }
718
                        }
719
                        if (!empty($queryAndOr)) {
720
                            $andOr = str_contains($queryOperator, ' not ')? 'and' : 'or';
721
                            $query [] = '((' . implode(') ' . $andOr . ' (', $queryAndOr) . '))';
722
                        }
723
                    }
724
                }
725
                else {
726
                    $query [] = "$queryFieldBinder $queryOperator";
727
                }
728
                
729
                $this->setBind($bind);
730
                $this->setBindTypes($bindType);
731
            }
732
            elseif (is_array($filter)) {
733
                $query [] = $this->getFilterCondition($filter, $whiteList, !$or);
734
            }
735
            else {
736
                throw new \Exception('A valid field property is required.', 400);
737
            }
738
        }
739
        
740
        return empty($query) ? null : '(' . implode($or ? ' or ' : ' and ', $query) . ')';
741
    }
742
    
743
    /**
744
     * Append the current model name alias to the field
745
     * So: field -> [Alias].[field]
746
     */
747
    public function appendModelName(string $field, ?string $modelName = null): string
748
    {
749
        $modelName ??= $this->getModelClassName();
750
        
751
        if (empty($field)) {
752
            return $field;
753
        }
754
        
755
        // Add the current model name by default
756
        $explode = explode(' ', $field);
757
        if (!strpos($field, '.') !== false) {
758
            $field = trim('[' . $modelName . '].[' . array_shift($explode) . '] ' . implode(' ', $explode));
759
        }
760
        elseif (strpos($field, ']') === false && strpos($field, '[') === false) {
761
            $field = trim('[' . implode('].[', explode('.', array_shift($explode))) . ']' . implode(' ', $explode));
762
        }
763
        
764
        return $field;
765
    }
766
    
767
    /**
768
     * Get Permission Condition
769
     *
770
     * @return null
771
     */
772
    protected function getPermissionCondition($type = null, $identity = null)
773
    {
774
        return null;
775
    }
776
    
777
    protected function fireGet($method)
778
    {
779
        // @todo
780
//        $eventRet = $this->eventsManager->fire('rest:before' . ucfirst($method), $this);
781
//        if ($eventRet !== false) {
782
//            $ret = $this->{$method}();
783
//            $eventRet = $this->eventsManager->fire('rest:after' . ucfirst($method), $this, $ret);
784
//        }
785
        
786
        $ret = $this->{$method}();
787
        $eventRet = $this->eventsManager->fire('rest:' . $method, $this, $ret);
788
        return $eventRet === false ? null : $eventRet ?? $ret;
789
    }
790
    
791
    /**
792
     * Get all conditions
793
     *
794
     * @return string
795
     * @throws \Exception
796
     */
797
    protected function getConditions()
798
    {
799
        $conditions = array_values(array_unique(array_filter([
800
            $this->fireGet('getSoftDeleteCondition'),
801
            $this->fireGet('getIdentityCondition'),
802
            $this->fireGet('getFilterCondition'),
803
            $this->fireGet('getSearchCondition'),
804
            $this->fireGet('getPermissionCondition'),
805
        ])));
806
        
807
        if (empty($conditions)) {
808
            $conditions [] = 1;
809
        }
810
        
811
        return '(' . implode(') and (', $conditions) . ')';
812
    }
813
    
814
    /**
815
     * Get having conditions
816
     */
817
    public function getHaving()
818
    {
819
        return null;
820
    }
821
    
822
    /**
823
     * Get a cache key from params
824
     *
825
     * @param array|null $params
826
     *
827
     * @return string|null
828
     */
829
    public function getCacheKey(?array $params = null): ?string
830
    {
831
        $params ??= $this->getParams();
832
        
833
        return Slug::generate(json_encode($params, JSON_UNESCAPED_SLASHES));
834
    }
835
    
836
    /**
837
     * Get cache setting
838
     *
839
     * @param array|null $params
840
     *
841
     * @return array|null
842
     */
843
    public function getCache(?array $params = null)
844
    {
845
        $params ??= $this->getParams();
846
        
847
        if (!empty($params['cache'])) {
848
            return [
849
                'lifetime' => (int)$params['cache'],
850
                'key' => $this->getCacheKey($params),
851
            ];
852
        }
853
        
854
        return null;
855
    }
856
    
857
    /**
858
     * Get find definition
859
     *
860
     * @return array
861
     * @throws \Exception
862
     */
863
    protected function getFind()
864
    {
865
        $find = [];
866
        $find['conditions'] = $this->fireGet('getConditions');
867
        $find['bind'] = $this->fireGet('getBind');
868
        $find['bindTypes'] = $this->fireGet('getBindTypes');
869
        $find['limit'] = $this->fireGet('getLimit');
870
        $find['offset'] = $this->fireGet('getOffset');
871
        $find['order'] = $this->fireGet('getOrder');
872
        $find['columns'] = $this->fireGet('getColumns');
873
        $find['distinct'] = $this->fireGet('getDistinct');
874
        $find['joins'] = $this->fireGet('getJoins');
875
        $find['group'] = $this->fireGet('getGroup');
876
        $find['having'] = $this->fireGet('getHaving');
877
        $find['cache'] = $this->fireGet('getCache');
878
        
879
        // fix for grouping by multiple fields, phalcon only allow string here
880
        foreach (['distinct', 'group', 'order'] as $findKey) {
881
            if (isset($find[$findKey]) && is_array($find[$findKey])) {
882
                $find[$findKey] = implode(', ', $find[$findKey]);
883
            }
884
        }
885
        
886
        return array_filter($find);
887
    }
888
    
889
    /**
890
     * Return find lazy loading config for count
891
     * @return array|string
892
     */
893
    protected function getFindCount($find = null)
894
    {
895
        $find ??= $this->getFind();
896
        if (isset($find['limit'])) {
897
            unset($find['limit']);
898
        }
899
        if (isset($find['offset'])) {
900
            unset($find['offset']);
901
        }
902
//        if (isset($find['group'])) {
903
//            unset($find['group']);
904
//        }
905
        
906
        return array_filter($find);
907
    }
908
    
909
    /**
910
     * Get Single from ID and Model Name
911
     *
912
     * @param string|int|null $id
913
     * @param string|null $modelName
914
     * @param string|array|null $with
915
     *
916
     * @return bool|Resultset|\Zemit\Mvc\Model
917
     */
918
    public function getSingle($id = null, $modelName = null, $with = [], $find = null, $appendCondition = true)
919
    {
920
        $id ??= (int)$this->getParam('id', 'int');
921
        $modelName ??= $this->getModelClassName();
922
        $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...
923
        $find ??= $this->getFind();
924
        
925
        $condition = '[' . $modelName . '].[id] = ' . (int)$id;
926
        
927
        if ($appendCondition) {
928
            $find['conditions'] .= (empty($find['conditions']) ? null : ' and ') . $condition;
929
        }
930
        else {
931
            $find['bind'] = [];
932
            $find['bindTypes'] = [];
933
            $find['conditions'] = $condition;
934
        }
935
        
936
        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

936
        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...
937
    }
938
    
939
    /**
940
     * Saving model automagically
941
     *
942
     * Note:
943
     * If a newly created entity can't be retrieved using the ->getSingle
944
     * method after it's creation, the entity will be returned directly
945
     *
946
     * @TODO Support Composite Primary Key*
947
     */
948
    protected function save(?int $id = null, ?ModelInterface $entity = null, ?array $post = null, ?string $modelName = null, ?array $whiteList = null, ?array $columnMap = null, ?array $with = null): array
949
    {
950
        $single = false;
951
        $retList = [];
952
        
953
        // Get the model name to play with
954
        $modelName ??= $this->getModelClassName();
955
        $post ??= $this->getParams();
956
        $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...
957
        $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...
958
        $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...
959
        $id = (int)$id;
960
        
961
        // Check if multi-d post
962
        if (!empty($id) || !isset($post[0]) || !is_array($post[0])) {
963
            $single = true;
964
            $post = [$post];
965
        }
966
        
967
        // Save each posts
968
        foreach ($post as $key => $singlePost) {
969
            $singlePostId = (!$single || empty($id)) ? $this->getParam('id', 'int', $this->getParam('int', 'int', $singlePost['id'] ?? null)) : $id;
970
            if (isset($singlePost['id'])) {
971
                unset($singlePost['id']);
972
            }
973
            
974
            /** @var \Zemit\Mvc\Model $singlePostEntity */
975
            $singlePostEntity = (!$single || !isset($entity)) ? $this->getSingle($singlePostId, $modelName, []) : $entity;
976
            
977
            // Create entity if not exists
978
            if (!$singlePostEntity && empty($singlePostId)) {
979
                $singlePostEntity = new $modelName();
980
            }
981
            
982
            if (!$singlePostEntity) {
983
                $ret = [
984
                    'saved' => false,
985
                    'messages' => [new Message('Entity id `' . $singlePostId . '` not found.', $modelName, 'NotFound', 404)],
986
                    'model' => $modelName,
987
                    'source' => (new $modelName())->getSource(),
988
                ];
989
            }
990
            else {
991
                // allow custom manipulations
992
                // @todo move this using events
993
                $this->beforeAssign($singlePostEntity, $singlePost, $whiteList, $columnMap);
994
                
995
                // assign & save
996
                $singlePostEntity->assign($singlePost, $whiteList, $columnMap);
997
                $ret = $this->saveEntity($singlePostEntity);
998
                
999
                // refetch & expose
1000
//                $fetchWith = $this->getSingle($singlePostEntity->getId(), $modelName, $with);
1001
//                $ret['single'] = $this->expose($fetchWith);
1002
                $fetchWith = $singlePostEntity->load($with ?? []);
1003
                $ret['single'] = $this->expose($fetchWith);
0 ignored issues
show
Bug introduced by
It seems like expose() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

1003
                /** @scrutinizer ignore-call */ 
1004
                $ret['single'] = $this->expose($fetchWith);
Loading history...
1004
            }
1005
            
1006
            if ($single) {
1007
                return $ret;
1008
            }
1009
            else {
1010
                $retList [] = $ret;
1011
            }
1012
        }
1013
        
1014
        return $retList;
1015
    }
1016
    
1017
    /**
1018
     * Allow overrides to add alter variables before entity assign & save
1019
     */
1020
    public function beforeAssign(ModelInterface &$entity, array &$post, ?array &$whiteList, ?array &$columnMap): void
1021
    {
1022
    }
1023
    
1024
    /**
1025
     * Save an entity and return an array of the result
1026
     *
1027
     */
1028
    public function saveEntity(ModelInterface $entity): array
1029
    {
1030
        $ret = [];
1031
        
1032
        $ret['saved'] = $entity->save();
1033
        $ret['messages'] = $entity->getMessages();
1034
        $ret['model'] = get_class($entity);
1035
        $ret['source'] = $entity->getSource();
1036
        $ret['entity'] = $entity; // @todo this is to fix a phalcon internal bug (503 segfault during eagerload)
1037
        $ret['single'] = $this->expose($entity, $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...
1038
        
1039
        return $ret;
1040
    }
1041
    
1042
    /**
1043
     * Try to find the appropriate model from the current controller name
1044
     *
1045
     * @param ?string $controllerName
1046
     * @param ?array $namespaces
1047
     * @param string $needle
1048
     *
1049
     * @return string|null|\Zemit\Mvc\Model
1050
     */
1051
    public function getModelNameFromController(string $controllerName = null, array $namespaces = null, string $needle = 'Models\\'): ?string
1052
    {
1053
        $controllerName ??= $this->dispatcher->getControllerName() ?? '';
1054
        $namespaces ??= $this->loader->getNamespaces() ?? [];
1055
        
1056
        $model = ucfirst(Helper::camelize(Helper::uncamelize($controllerName)));
1057
        if (!class_exists($model)) {
1058
            foreach ($namespaces as $namespace => $path) {
1059
                $possibleModel = preg_replace('/\\\\+/', '\\', $namespace . '\\' . $model); // @todo check if phalcon namespaces are always going to end with a trailing backslash
1060
                if (str_ends_with($namespace, $needle) && class_exists($possibleModel)) {
1061
                    $model = $possibleModel;
1062
                }
1063
            }
1064
        }
1065
        
1066
        return class_exists($model) && new $model() instanceof ModelInterface ? $model : null;
1067
    }
1068
}
1069