ModelClass   F
last analyzed

Complexity

Total Complexity 74

Size/Duplication

Total Lines 478
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 180
c 3
b 0
f 0
dl 0
loc 478
rs 2.48
wmc 74

19 Methods

Rating   Name   Duplication   Size   Complexity  
A all() 0 14 3
A codeModelSearch() 0 6 2
A codeModelAll() 0 12 3
A newCode() 0 22 4
A totalSum() 0 18 5
A count() 0 18 5
B saveInsert() 0 41 6
A get() 0 4 2
A primaryDescriptionColumn() 0 4 2
A save() 0 22 5
A getOrderBy() 0 10 2
A loadFromCode() 0 10 2
A primaryDescription() 0 4 1
A getRecord() 0 7 4
A exists() 0 6 2
B url() 0 23 7
A delete() 0 28 4
A saveUpdate() 0 34 5
B test() 0 25 10

How to fix   Complexity   

Complex Class

Complex classes like ModelClass often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ModelClass, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of FacturaScripts
4
 * Copyright (C) 2013-2024 Carlos Garcia Gomez <[email protected]>
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as
8
 * published by the Free Software Foundation, either version 3 of the
9
 * License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU Lesser General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Lesser General Public License
17
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18
 */
19
20
namespace FacturaScripts\Core\Model\Base;
21
22
use FacturaScripts\Core\Base\DataBase\DataBaseWhere;
23
use FacturaScripts\Core\Cache;
24
use FacturaScripts\Core\Tools;
25
use FacturaScripts\Core\WorkQueue;
26
use FacturaScripts\Dinamic\Model\CodeModel;
27
28
/**
29
 * The class from which all models inherit, connects to the database,
30
 * check the structure of the table and if necessary create or adapt.
31
 *
32
 * @author Carlos García Gómez <[email protected]>
33
 */
34
abstract class ModelClass extends ModelCore
35
{
36
    /**
37
     * Returns all models that correspond to the selected filters.
38
     *
39
     * @param array $where filters to apply to model records.
40
     * @param array $order fields to use in the sorting. For example ['code' => 'ASC']
41
     * @param int $offset
42
     * @param int $limit
43
     *
44
     * @return static[]
45
     */
46
    public static function all(array $where = [], array $order = [], int $offset = 0, int $limit = 50): array
47
    {
48
        // si todavía no se ha comprobado la tabla, inicializamos la clase
49
        if (!in_array(static::tableName(), self::$checkedTables)) {
50
            new static();
51
        }
52
53
        $modelList = [];
54
        $sql = 'SELECT * FROM ' . static::tableName() . DataBaseWhere::getSQLWhere($where) . self::getOrderBy($order);
55
        foreach (self::$dataBase->selectLimit($sql, $limit, $offset) as $row) {
56
            $modelList[] = new static($row);
57
        }
58
59
        return $modelList;
60
    }
61
62
    /**
63
     * Allows to use this model as source in CodeModel special model.
64
     *
65
     * @param string $fieldCode
66
     *
67
     * @return CodeModel[]
68
     */
69
    public function codeModelAll(string $fieldCode = ''): array
70
    {
71
        $results = [];
72
        $field = empty($fieldCode) ? static::primaryColumn() : $fieldCode;
73
74
        $sql = 'SELECT DISTINCT ' . $field . ' AS code, ' . $this->primaryDescriptionColumn() . ' AS description '
75
            . 'FROM ' . static::tableName() . ' ORDER BY 2 ASC';
76
        foreach (self::$dataBase->selectLimit($sql, CodeModel::ALL_LIMIT) as $d) {
77
            $results[] = new CodeModel($d);
78
        }
79
80
        return $results;
81
    }
82
83
    /**
84
     * Allows to use this model as source in CodeModel special model.
85
     *
86
     * @param string $query
87
     * @param string $fieldCode
88
     * @param DataBaseWhere[] $where
89
     *
90
     * @return CodeModel[]
91
     */
92
    public function codeModelSearch(string $query, string $fieldCode = '', array $where = []): array
93
    {
94
        $field = empty($fieldCode) ? static::primaryColumn() : $fieldCode;
95
        $fields = $field . '|' . $this->primaryDescriptionColumn();
96
        $where[] = new DataBaseWhere($fields, mb_strtolower($query, 'UTF8'), 'LIKE');
97
        return CodeModel::all(static::tableName(), $field, $this->primaryDescriptionColumn(), false, $where);
98
    }
99
100
    /**
101
     * Returns the number of records in the model that meet the condition.
102
     *
103
     * @param DataBaseWhere[] $where filters to apply to model records.
104
     *
105
     * @return int
106
     */
107
    public function count(array $where = []): int
108
    {
109
        $sql = 'SELECT COUNT(1) AS total FROM ' . static::tableName();
110
111
        if ($where) {
112
            $data = self::$dataBase->select($sql . DataBaseWhere::getSQLWhere($where));
113
            return empty($data) ? 0 : (int)$data[0]['total'];
114
        }
115
116
        $key = 'model-' . $this->modelClassName() . '-count';
117
        $count = Cache::get($key);
118
        if (is_null($count)) {
119
            $data = self::$dataBase->select($sql);
120
            $count = empty($data) ? 0 : (int)$data[0]['total'];
121
            Cache::set($key, $count);
122
        }
123
124
        return $count;
125
    }
126
127
    /**
128
     * Remove the model data from the database.
129
     *
130
     * @return bool
131
     */
132
    public function delete(): bool
133
    {
134
        if (null === $this->primaryColumnValue()) {
135
            return true;
136
        }
137
138
        if ($this->pipeFalse('deleteBefore') === false) {
139
            return false;
140
        }
141
142
        $sql = 'DELETE FROM ' . static::tableName() . ' WHERE ' . static::primaryColumn()
143
            . ' = ' . self::$dataBase->var2str($this->primaryColumnValue()) . ';';
144
145
        if (false === self::$dataBase->exec($sql)) {
146
            return false;
147
        }
148
149
        Cache::deleteMulti('model-' . $this->modelClassName() . '-');
150
        Cache::deleteMulti('join-model-');
151
        Cache::deleteMulti('table-' . static::tableName() . '-');
152
153
        WorkQueue::send(
154
            'Model.' . $this->modelClassName() . '.Delete',
155
            $this->primaryColumnValue(),
156
            $this->toArray()
157
        );
158
159
        return $this->pipeFalse('delete');
160
    }
161
162
    /**
163
     * Returns true if the model data is stored in the database.
164
     *
165
     * @return bool
166
     */
167
    public function exists(): bool
168
    {
169
        $sql = 'SELECT 1 FROM ' . static::tableName() . ' WHERE ' . static::primaryColumn()
170
            . ' = ' . self::$dataBase->var2str($this->primaryColumnValue()) . ';';
171
172
        return !empty($this->primaryColumnValue()) && self::$dataBase->select($sql);
173
    }
174
175
    /**
176
     * Returns the model whose primary column corresponds to the value $cod
177
     *
178
     * @param string $code
179
     *
180
     * @return static|false
181
     */
182
    public function get($code)
183
    {
184
        $data = $this->getRecord($code);
185
        return empty($data) ? false : new static($data[0]);
186
    }
187
188
    /**
189
     * Fill the class with the registry values
190
     * whose primary column corresponds to the value $cod, or according to the condition
191
     * where indicated, if value is not reported in $cod.
192
     * Initializes the values of the class if there is no record that
193
     * meet the above conditions.
194
     * Returns True if the record exists and False otherwise.
195
     *
196
     * @param string $code
197
     * @param array $where
198
     * @param array $order
199
     *
200
     * @return bool
201
     */
202
    public function loadFromCode($code, array $where = [], array $order = []): bool
203
    {
204
        $data = $this->getRecord($code, $where, $order);
205
        if (empty($data)) {
206
            $this->clear();
207
            return false;
208
        }
209
210
        $this->loadFromData($data[0]);
211
        return true;
212
    }
213
214
    /**
215
     * Returns the following code for the reported field or the primary key of the model.
216
     *
217
     * @param string $field
218
     * @param array $where
219
     *
220
     * @return int
221
     */
222
    public function newCode(string $field = '', array $where = [])
223
    {
224
        // if not field value take PK Field
225
        if (empty($field)) {
226
            $field = static::primaryColumn();
227
        }
228
229
        // get fields list
230
        $modelFields = $this->getModelFields();
231
232
        // Set Cast to Integer if field it's not
233
        if (false === in_array($modelFields[$field]['type'], ['integer', 'int', 'serial'])) {
234
            // Set Where to Integers values only
235
            $where[] = new DataBaseWhere($field, '^-?[0-9]+$', 'REGEXP');
236
            $field = self::$dataBase->getEngine()->getSQL()->sql2Int($field);
237
        }
238
239
        // Search for new code value
240
        $sqlWhere = DataBaseWhere::getSQLWhere($where);
241
        $sql = 'SELECT MAX(' . $field . ') as cod FROM ' . static::tableName() . $sqlWhere . ';';
242
        $data = self::$dataBase->select($sql);
243
        return empty($data) ? 1 : 1 + (int)$data[0]['cod'];
244
    }
245
246
    /**
247
     * Returns the name of the column that describes the model, such as name, description...
248
     *
249
     * @return string
250
     */
251
    public function primaryDescriptionColumn(): string
252
    {
253
        $fields = $this->getModelFields();
254
        return isset($fields['descripcion']) ? 'descripcion' : static::primaryColumn();
255
    }
256
257
    /**
258
     * Descriptive identifier for humans of the data record
259
     *
260
     * @return string
261
     */
262
    public function primaryDescription()
263
    {
264
        $field = $this->primaryDescriptionColumn();
265
        return $this->{$field} ?? (string)$this->primaryColumnValue();
266
    }
267
268
    /**
269
     * Stores the model data in the database.
270
     *
271
     * @return bool
272
     */
273
    public function save(): bool
274
    {
275
        if ($this->pipeFalse('saveBefore') === false) {
276
            return false;
277
        }
278
279
        if (false === $this->test()) {
280
            return false;
281
        }
282
283
        $done = $this->exists() ? $this->saveUpdate() : $this->saveInsert();
284
        if (false === $done) {
285
            return false;
286
        }
287
288
        WorkQueue::send(
289
            'Model.' . $this->modelClassName() . '.Save',
290
            $this->primaryColumnValue(),
291
            $this->toArray()
292
        );
293
294
        return $this->pipeFalse('save');
295
    }
296
297
    /**
298
     * Returns true if there are no errors in the values of the model properties.
299
     * It runs inside the save method.
300
     *
301
     * @return bool
302
     */
303
    public function test(): bool
304
    {
305
        if ($this->pipeFalse('testBefore') === false) {
306
            return false;
307
        }
308
309
        // comprobamos que los campos no nulos tengan algún valor asignado
310
        $fields = $this->getModelFields();
311
        if (empty($fields)) {
312
            return false;
313
        }
314
        $return = true;
315
        foreach ($fields as $key => $value) {
316
            if ($key == static::primaryColumn()) {
317
                $this->{$key} = empty($this->{$key}) ? null : $this->{$key};
318
            } elseif (null === $value['default'] && $value['is_nullable'] === 'NO' && $this->{$key} === null) {
319
                Tools::log()->warning('field-can-not-be-null', ['%fieldName%' => $key, '%tableName%' => static::tableName()]);
320
                $return = false;
321
            }
322
        }
323
        if (false === $return) {
324
            return false;
325
        }
326
327
        return $this->pipeFalse('test');
328
    }
329
330
    public function totalSum(string $field, array $where = []): float
331
    {
332
        $sql = 'SELECT SUM(' . self::$dataBase->escapeColumn($field) . ') AS total FROM ' . static::tableName();
333
334
        if ($where) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $where of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
335
            $data = self::$dataBase->select($sql . DataBaseWhere::getSQLWhere($where));
336
            return empty($data) ? 0 : (int)$data[0]['total'];
337
        }
338
339
        $key = 'model-' . $this->modelClassName() . '-' . $field . '-total-sum';
340
        $sum = Cache::get($key);
341
        if (is_null($sum)) {
342
            $data = self::$dataBase->select($sql);
343
            $sum = empty($data) ? 0 : (float)$data[0]['total'];
344
            Cache::set($key, $sum);
345
        }
346
347
        return $sum;
348
    }
349
350
    /**
351
     * Returns the url where to see / modify the data.
352
     *
353
     * @param string $type
354
     * @param string $list
355
     *
356
     * @return string
357
     */
358
    public function url(string $type = 'auto', string $list = 'List'): string
359
    {
360
        $value = $this->primaryColumnValue();
361
        $model = $this->modelClassName();
362
363
        $return = $this->pipe('url', $type, $list);
364
        if ($return) {
365
            return $return;
366
        }
367
368
        switch ($type) {
369
            case 'edit':
370
                return is_null($value) ? 'Edit' . $model : 'Edit' . $model . '?code=' . rawurlencode($value);
371
372
            case 'list':
373
                return $list . $model;
374
375
            case 'new':
376
                return 'Edit' . $model;
377
        }
378
379
        // default
380
        return empty($value) ? $list . $model : 'Edit' . $model . '?code=' . rawurlencode($value);
381
    }
382
383
    /**
384
     * Insert the model data in the database.
385
     *
386
     * @param array $values
387
     *
388
     * @return bool
389
     */
390
    protected function saveInsert(array $values = []): bool
391
    {
392
        if ($this->pipeFalse('saveInsertBefore') === false) {
393
            return false;
394
        }
395
396
        $insertFields = [];
397
        $insertValues = [];
398
        foreach ($this->getModelFields() as $field) {
399
            if (isset($this->{$field['name']})) {
400
                $fieldName = $field['name'];
401
                $fieldValue = $values[$fieldName] ?? $this->{$fieldName};
402
403
                $insertFields[] = self::$dataBase->escapeColumn($fieldName);
404
                $insertValues[] = self::$dataBase->var2str($fieldValue);
405
            }
406
        }
407
408
        $sql = 'INSERT INTO ' . static::tableName() . ' (' . implode(',', $insertFields)
409
            . ') VALUES (' . implode(',', $insertValues) . ');';
410
        if (false === self::$dataBase->exec($sql)) {
411
            return false;
412
        }
413
414
        if ($this->primaryColumnValue() === null) {
415
            $this->{static::primaryColumn()} = self::$dataBase->lastval();
416
        } else {
417
            self::$dataBase->updateSequence(static::tableName(), $this->getModelFields());
418
        }
419
420
        Cache::deleteMulti('model-' . $this->modelClassName() . '-');
421
        Cache::deleteMulti('join-model-');
422
        Cache::deleteMulti('table-' . static::tableName() . '-');
423
424
        WorkQueue::send(
425
            'Model.' . $this->modelClassName() . '.Insert',
426
            $this->primaryColumnValue(),
427
            $this->toArray()
428
        );
429
430
        return $this->pipeFalse('saveInsert');
431
    }
432
433
    /**
434
     * Update the model data in the database.
435
     *
436
     * @param array $values
437
     *
438
     * @return bool
439
     */
440
    protected function saveUpdate(array $values = []): bool
441
    {
442
        if ($this->pipeFalse('saveUpdateBefore') === false) {
443
            return false;
444
        }
445
446
        $sql = 'UPDATE ' . static::tableName();
447
        $coma = ' SET';
448
449
        foreach ($this->getModelFields() as $field) {
450
            if ($field['name'] !== static::primaryColumn()) {
451
                $fieldName = $field['name'];
452
                $fieldValue = $values[$fieldName] ?? $this->{$fieldName};
453
                $sql .= $coma . ' ' . self::$dataBase->escapeColumn($fieldName) . ' = ' . self::$dataBase->var2str($fieldValue);
454
                $coma = ', ';
455
            }
456
        }
457
458
        $sql .= ' WHERE ' . static::primaryColumn() . ' = ' . self::$dataBase->var2str($this->primaryColumnValue()) . ';';
459
        if (false === self::$dataBase->exec($sql)) {
460
            return false;
461
        }
462
463
        Cache::deleteMulti('model-' . $this->modelClassName() . '-');
464
        Cache::deleteMulti('join-model-');
465
        Cache::deleteMulti('table-' . static::tableName() . '-');
466
467
        WorkQueue::send(
468
            'Model.' . $this->modelClassName() . '.Update',
469
            $this->primaryColumnValue(),
470
            $this->toArray()
471
        );
472
473
        return $this->pipeFalse('saveUpdate');
474
    }
475
476
    /**
477
     * Convert an array of filters order by in string.
478
     *
479
     * @param array $order
480
     *
481
     * @return string
482
     */
483
    private static function getOrderBy(array $order): string
484
    {
485
        $result = '';
486
        $coma = ' ORDER BY ';
487
        foreach ($order as $key => $value) {
488
            $result .= $coma . $key . ' ' . $value;
489
            $coma = ', ';
490
        }
491
492
        return $result;
493
    }
494
495
    /**
496
     * Read the record whose primary column corresponds to the value $cod
497
     * or the first that meets the indicated condition.
498
     *
499
     * @param string $code
500
     * @param array $where
501
     * @param array $order
502
     *
503
     * @return array
504
     */
505
    private function getRecord($code, array $where = [], array $order = []): array
506
    {
507
        $sqlWhere = empty($where) ?
508
            ' WHERE ' . static::primaryColumn() . ' = ' . self::$dataBase->var2str($code) :
509
            DataBaseWhere::getSQLWhere($where);
510
        $sql = 'SELECT * FROM ' . static::tableName() . $sqlWhere . self::getOrderBy($order);
511
        return empty($code) && empty($where) ? [] : self::$dataBase->selectLimit($sql, 1);
512
    }
513
}
514