Passed
Push — master ( d98c91...5c9a86 )
by Bruno
08:10
created

Model   F

Complexity

Total Complexity 64

Size/Duplication

Total Lines 489
Duplicated Lines 0 %

Test Coverage

Coverage 70.59%

Importance

Changes 14
Bugs 5 Features 1
Metric Value
eloc 173
c 14
b 5
f 1
dl 0
loc 489
rs 3.28
ccs 96
cts 136
cp 0.7059
wmc 64

27 Methods

Rating   Name   Duplication   Size   Complexity  
A getField() 0 3 1
A fromStruct() 0 5 1
A getName() 0 3 1
A getRenderables() 0 3 1
A serialize() 0 17 1
A toJSON() 0 7 2
A __construct() 0 3 1
A appendField() 0 4 1
A getAllFields() 0 3 1
B getFields() 0 22 8
C validate() 0 72 14
A fromJSON() 0 9 2
A appendFields() 0 6 2
A getRenderable() 0 3 1
A fromJSONFile() 0 7 2
A filterField() 0 5 1
A getData() 0 3 1
A create() 0 11 3
A viewable() 0 8 1
A toGraphqlQuery() 0 10 2
A getRandom() 0 7 2
A editableNodes() 0 8 1
A viewableNodes() 0 8 1
A toGraphqlTypeDefinition() 0 27 4
A parseStruct() 0 17 6
A editable() 0 8 1
A getDefault() 0 7 2

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