Model   F
last analyzed

Complexity

Total Complexity 73

Size/Duplication

Total Lines 525
Duplicated Lines 0 %

Test Coverage

Coverage 71.81%

Importance

Changes 22
Bugs 5 Features 2
Metric Value
wmc 73
eloc 179
c 22
b 5
f 2
dl 0
loc 525
ccs 135
cts 188
cp 0.7181
rs 2.56

28 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A fromStruct() 0 5 1
A getName() 0 3 1
A getRenderables() 0 3 1
A getAllFields() 0 3 1
B getFields() 0 22 8
A fromJSON() 0 9 2
A getRenderable() 0 3 1
A fromJSONFile() 0 7 2
A getData() 0 3 1
A create() 0 11 3
A getField() 0 3 1
A appendField() 0 4 1
A firstField() 0 8 3
A filterField() 0 5 1
A mapFields() 0 7 2
A appendRenderable() 0 4 1
A serialize() 0 20 1
A toJSON() 0 7 2
C validate() 0 76 14
A viewable() 0 8 1
A getRandom() 0 21 6
A editableNodes() 0 8 1
A viewableNodes() 0 8 1
A editable() 0 8 1
B parseStruct() 0 24 9
A appendFields() 0 6 2
A getDefault() 0 11 4

How to fix   Complexity   

Complex Class

Complex classes like Model 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 Model, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace Formularium;
4
5
use Formularium\Exception\Exception;
6
use Formularium\Exception\NoRandomException;
7
use Formularium\Factory\ValidatorFactory;
8
9
/**
10
 * Model class, representing a whole object.
11
 */
12
class Model
13
{
14
    use ExtradataTrait;
0 ignored issues
show
introduced by
The trait Formularium\ExtradataTrait requires some properties which are not provided by Formularium\Model: $value, $args
Loading history...
15
16
    /**
17
     * @var string
18
     */
19
    public $name;
20
21
    /**
22
     * @var Field[]
23
     */
24
    public $fields = [];
25
26
    /**
27
     * @var array
28
     */
29
    public $renderable = [];
30
31
    /**
32
     * Model data being processed.
33
     * @var array
34
     */
35
    protected $_data = [];
36
37
    /**
38
     * Model data being processed.
39
     * @var string[]|callable|null
40
     */
41
    protected $_restrictFields = null;
42
43
    /**
44
     *
45
     * @param string $name
46
     * @throws Exception
47
     */
48 67
    public function __construct(string $name = '')
49
    {
50 67
        $this->name = $name;
51 67
    }
52
53
    /**
54
     * Loads model from JSON file.
55
     *
56
     * @param array $struct
57
     * @return Model
58
     */
59 57
    public static function fromStruct(array $struct): Model
60
    {
61 57
        $m = new self('');
62 57
        $m->parseStruct($struct);
63 57
        return $m;
64
    }
65
66
    /**
67
     * Loads model from JSON file.
68
     *
69
     * @param string $name The JSON filename.
70
     * @return Model
71
     */
72
    public static function fromJSONFile(string $name): Model
73
    {
74
        $json = file_get_contents($name); // TODO: path
75
        if ($json === false) {
76
            throw new Exception('File not found');
77
        }
78
        return static::fromJSON($json);
79
    }
80
81
    /**
82
     * Loads model from JSON string
83
     *
84
     * @param string $json The JSON string.
85
     * @return Model
86
     */
87 5
    public static function fromJSON(string $json): Model
88
    {
89 5
        $data = \json_decode($json, true);
90 5
        if ($data === null) {
91 1
            throw new Exception('Invalid JSON format');
92
        }
93 4
        $m = new self('');
94 4
        $m->parseStruct($data);
95 1
        return $m;
96
    }
97
98
    /**
99
     * @param string $name
100
     * @param array[]|Field[] $fields
101
     * @return Model
102
     */
103
    public static function create(string $name, array $fields = []): Model
104
    {
105
        $m = new self($name);
106
        foreach ($fields as $fieldName => $fieldData) {
107
            if ($fieldData instanceof Field) {
108
                $m->fields[$fieldData->getName()] = $fieldData;
109
            } else {
110
                $m->fields[$fieldName] = Field::getFromData($fieldName, $fieldData);
111
            }
112
        }
113
        return $m;
114
    }
115
116 16
    public function getName(): string
117
    {
118 16
        return $this->name;
119
    }
120
121 1
    public function getAllFields(): array
122
    {
123 1
        return $this->fields;
124
    }
125
126
    /**
127
     * @param string[]|callable $restrictFields If present, restrict rendered fields. Can either
128
     * be an array of strings (field names) or a callback which is called for each field.
129
     * @return Field[]
130
     */
131 23
    public function getFields($restrictFields = null): array
132
    {
133 23
        if ($restrictFields === null) {
134 21
            $restrictFields = $this->_restrictFields;
135
        }
136 23
        if ($restrictFields === null) {
137 21
            return $this->fields;
138
        }
139
140 2
        $fields = [];
141 2
        foreach ($this->fields as $field) {
142
            /**
143
             * @var Field $field
144
             */
145 2
            if (is_array($restrictFields) && !in_array($field->getName(), $restrictFields)) {
146 1
                continue;
147 2
            } elseif (is_callable($restrictFields) && !$restrictFields($field, $this)) {
148 1
                continue;
149
            }
150 2
            $fields[$field->getName()] = $field;
151
        }
152 2
        return $fields;
153
    }
154
155 13
    public function getData(): array
156
    {
157 13
        return $this->_data;
158
    }
159
160 3
    public function getRenderables(): array
161
    {
162 3
        return $this->renderable;
163
    }
164
165
    /**
166
     * @param string $name
167
     * @param mixed $default
168
     * @return mixed
169
     */
170 3
    public function getRenderable(string $name, $default)
171
    {
172 3
        return $this->renderable[$name] ?? $default;
173
    }
174
175
    /**
176
     * @param string $name
177
     * @param mixed $value
178
     * @return self
179
     */
180
    public function appendRenderable(string $name, $value): self
181
    {
182
        $this->renderable[$name] = $value;
183
        return $this;
184
    }
185
186 31
    public function getField(string $name): Field
187
    {
188 31
        return $this->fields[$name];
189
    }
190
191
    /**
192
     * Returns the first field matching a function.
193
     *
194
     * @param callable $function. Receives a Field as argument.
195
     * @return Field
196
     */
197 1
    public function firstField(callable $function): ?Field
198
    {
199 1
        foreach ($this->getFields() as $f) {
200 1
            if ($function($f)) {
201 1
                return $f;
202
            }
203
        }
204 1
        return null;
205
    }
206
207
    /**
208
     * filter operation for fields that return true for callable.
209
     *
210
     * @param callable $function. Receives a Field as argument.
211
     * @return Field[]
212
     */
213 1
    public function filterField(callable $function): array
214
    {
215 1
        return array_filter(
216 1
            $this->fields,
217
            $function
218
        );
219
    }
220
221
    /**
222
     * @param callable $function
223
     * @return array
224
     */
225
    public function mapFields(callable $function): array
226
    {
227
        $data = [];
228
        foreach ($this->fields as $field) {
229
            $data[] = $function($field);
230
        }
231
        return $data;
232
    }
233
234
    public function appendField(Field $f): self
235
    {
236
        $this->fields[$f->getName()] = $f;
237
        return $this;
238
    }
239
240
    /**
241
     * @param Field[] $fields
242
     * @return self
243
     */
244 6
    public function appendFields(array $fields): self
245
    {
246 6
        foreach ($fields as $f) {
247 6
            $this->fields[$f->getName()] = $f;
248
        }
249 6
        return $this;
250
    }
251
252
    /**
253
     * Validates a set of data against this model.
254
     *
255
     * @param array $data A field name => data array.
256
     * @return array
257
     */
258 8
    public function validate(array $data): array
259
    {
260 8
        $this->_data = $data;
261 8
        $validate = [];
262 8
        $errors = [];
263
264
        // validate data
265 8
        foreach ($data as $name => $d) {
266
            // expected?
267 8
            if (!array_key_exists($name, $this->fields)) {
268 1
                $errors[$name] = "Field $name does not exist in this model";
269 1
                continue;
270
            }
271
272
            // call the datatype validator
273 8
            $field = $this->fields[$name];
274
            try {
275 8
                $validate[$name] = $field->getDatatype()->validate($d, $this);
276
            } catch (Exception $e) {
277
                $errors[$name] = $e->getMessage();
278
            }
279
280
            // call class validators.
281 8
            foreach ($field->getValidators() as $validatorName => $options) {
282
                /**
283
                 * @var string $validatorName
284
                 */
285
                // special case
286 8
                if ($validatorName === Datatype::REQUIRED) {
287 1
                    continue;
288
                }
289
290
                try {
291 8
                    $validate[$name] = ValidatorFactory::class($validatorName)::validate(
292 8
                        $validate[$name],
293
                        $options,
294
                        $this
295
                    );
296 5
                } catch (Exception $e) {
297 5
                    $errors[$name] = $e->getMessage();
298
                }
299
            }
300
        }
301
302
        // now validate fields, since you may have some that were not in data.
303 8
        foreach ($this->fields as $name => $field) {
304
            // if in field list but not in data
305 8
            if (!array_key_exists($name, $data)) {
306
                // call class validators.
307 5
                foreach ($field->getValidators() as $validatorName => $options) {
308
                    /**
309
                     * @var string $validatorName
310
                     */
311 5
                    if ($validatorName === Datatype::REQUIRED) {
312 1
                        if (!array_key_exists($name, $validate)
313 1
                            && !array_key_exists($name, $errors)
314
                        ) {
315
                            $errors[$name] = "Field $name is missing";
316
                        }
317 1
                        continue;
318
                    }
319
320
                    try {
321 5
                        ValidatorFactory::class($validatorName)::validate(
322 5
                            null,
323
                            $options,
324
                            $this
325
                        );
326 3
                    } catch (Exception $e) {
327 3
                        $errors[$name] = $e->getMessage();
328
                    }
329
                }
330
            }
331
        }
332 8
        $this->_data = [];
333 8
        return ['validated' => $validate, 'errors' => $errors];
334
    }
335
336
    /**
337
     * Serializes this model to JSON.
338
     *
339
     * @return array
340
     */
341 1
    public function serialize(): array
342
    {
343 1
        $fields = array_map(
344
            function (Field $f) {
345
                return [
346 1
                    'datatype' => $f->getDatatype()->getName(),
347 1
                    'validators' => $f->getValidators(),
348 1
                    'renderable' => $f->getRenderables(),
349 1
                    'extradata' => $f->getExtradataSerialize()
350
                ];
351 1
            },
352 1
            $this->fields
353
        );
354
        $model = [
355 1
            'name' => $this->name,
356 1
            'renderable' => $this->renderable,
357 1
            'extradata' => $this->getExtradataSerialize(),
358 1
            'fields' => $fields
359
        ];
360 1
        return $model;
361
    }
362
363 1
    public function toJSON(): string
364
    {
365 1
        $t = json_encode($this->serialize());
366 1
        if (!$t) {
367
            throw new Exception('Cannot serialize');
368
        }
369 1
        return $t;
370
    }
371
372
    /**
373
     * Renders a readonly view of the model with given data.
374
     *
375
     * @param FrameworkComposer $composer
376
     * @param array $modelData Actual data for the fields to render. Can be empty.
377
     * @param string[]|callable $restrictFields If present, restrict rendered fields. Can either
378
     * be an array of strings (field names) or a callback which is called for each field.
379
     * Callable signature: (Field $field, Model $m): boolean
380
     * @return HTMLNode[]
381
     */
382
    public function viewableNodes(FrameworkComposer $composer, array $modelData, $restrictFields = null): array
383
    {
384
        $this->_data = $modelData;
385
        $this->_restrictFields = $restrictFields;
386
        $r = $composer->viewableNodes($this, $modelData);
387
        $this->_data = [];
388
        $this->_restrictFields = null;
389
        return $r;
390
    }
391
392
    /**
393
     * Renders a readonly view of the model with given data.
394
     *
395
     * @param FrameworkComposer $composer
396
     * @param array $modelData Actual data for the fields to render. Can be empty.
397
     * @param string[]|callable $restrictFields If present, restrict rendered fields. Can either
398
     * be an array of strings (field names) or a callback which is called for each field.
399
     * Callable signature: (Field $field, Model $m, array $modelData): boolean
400
     * @return string
401
     */
402 2
    public function viewable(FrameworkComposer $composer, array $modelData, $restrictFields = null): string
403
    {
404 2
        $this->_data = $modelData;
405 2
        $this->_restrictFields = $restrictFields;
406 2
        $r = $composer->viewable($this, $modelData);
407 2
        $this->_data = [];
408 2
        $this->_restrictFields = null;
409 2
        return $r;
410
    }
411
412
    /**
413
     * Renders a form view of the model with given data.
414
     *
415
     * @param FrameworkComposer $composer
416
     * @param array $modelData
417
     * @param string[]|callable $restrictFields If present, restrict rendered fields. Can either
418
     * be an array of strings (field names) or a callback which is called for each field.
419
     * Callable signature: (Field $field, Model $m, array $modelData): boolean
420
     * @return HTMLNode[]
421
     */
422
    public function editableNodes(FrameworkComposer $composer, array $modelData = [], $restrictFields = null): array
423
    {
424
        $this->_data = $modelData;
425
        $this->_restrictFields = $restrictFields;
426
        $r = $composer->editableNodes($this, $modelData);
427
        $this->_data = [];
428
        $this->_restrictFields = null;
429
        return $r;
430
    }
431
432
    /**
433
     * Renders a form view of the model with given data.
434
     *
435
     * @param FrameworkComposer $composer
436
     * @param array $modelData
437
     * @param string[]|callable $restrictFields If present, restrict rendered fields. Can either
438
     * be an array of strings (field names) or a callback which is called for each field.
439
     * Callable signature: (Field $field, Model $m, array $modelData): boolean
440
     * @return string
441
     */
442
    public function editable(FrameworkComposer $composer, array $modelData = [], $restrictFields = null): string
443
    {
444
        $this->_data = $modelData;
445
        $this->_restrictFields = $restrictFields;
446
        $r = $composer->editable($this, $modelData);
447
        $this->_data = [];
448
        $this->_restrictFields = null;
449
        return $r;
450
    }
451
452
    /**
453
     * Generates random data for this model
454
     *
455
     * @param string[]|callable $restrictFields If present, restrict rendered fields. Can either
456
     * be an array of strings (field names) or a callback which is called for each field.
457
     * If a list of strings, only process the field names present in that list.
458
     * If it's a callable, it may fill `$data` with a random value
459
     * (you can call `$field->getDatatype()->getRandom()` for the default). The entire $data
460
     * array is provided (note it should be a reference in the callable parameter!) so you
461
     * can use a different key than `$field->getName()` or fill more than one value.
462
     * Callable signature: (Field $field, Model $m, array &$data): void
463
     * @return array An associative array field name => data.
464
     */
465 1
    public function getRandom($restrictFields = null): array
466
    {
467 1
        $data = [];
468
469 1
        foreach ($this->fields as $field) {
470
            /**
471
             * @var Field $field
472
             */
473 1
            if (is_array($restrictFields) && !in_array($field->getName(), $restrictFields)) {
474
                continue;
475 1
            } elseif (is_callable($restrictFields)) {
476
                try {
477
                    $restrictFields($field, $this, $data);
478
                } catch (NoRandomException $e) {
479
                    // pass
480
                }
481
            } else {
482 1
                $data[$field->getName()] = $field->getDatatype()->getRandom();
483
            }
484
        }
485 1
        return $data;
486
    }
487
488
    /**
489
     * Returns an array with the default values of each field
490
     *
491
     * @return array Field name => value
492
     */
493 8
    public function getDefault(): array
494
    {
495 8
        $data = [];
496 8
        foreach ($this->getFields() as $f) {
497 8
            $d = $f->getDatatype()->getDefault();
498 8
            if ($d === '' || $d === null) {
499 8
                $d = $f->getRenderable(RenderableParameter::DEFAULTVALUE, null);
500
            }
501 8
            $data[$f->getName()] = $d;
502
        }
503 8
        return $data;
504
    }
505
506
    /**
507
     * Parses struct
508
     *
509
     * @param array $data
510
     * @throws Exception
511
     * @return void
512
     */
513 61
    protected function parseStruct(array $data)
514
    {
515 61
        if (!array_key_exists('name', $data)) {
516 1
            throw new Exception('Missing name in model');
517
        }
518 60
        if (!array_key_exists('fields', $data)) {
519 1
            throw new Exception('Missing fields in model');
520
        }
521 59
        $this->name = $data['name'];
522 59
        foreach ($data['fields'] as $fieldName => $fieldData) {
523 59
            $this->fields[$fieldName] = Field::getFromData($fieldName, $fieldData);
524
        }
525 58
        if (array_key_exists('renderable', $data)) {
526 2
            if (!is_array($data['renderable'])) {
527
                throw new Exception('Model renderable must be an array');
528
            }
529 2
            $this->renderable = $data['renderable'];
530
        }
531 58
        if (array_key_exists('extradata', $data)) {
532 1
            if (!is_array($data['extradata'])) {
533
                throw new Exception('Model extradata must be an array');
534
            }
535 1
            foreach ($data['extradata'] as $mData) {
536 1
                $this->extradata[] = new Extradata($mData['name'], $mData['args']);
537
            }
538
        }
539 58
    }
540
}
541