Passed
Push — master ( 33a1b2...203229 )
by Bruno
09:06
created

Model::validate()   C

Complexity

Conditions 14
Paths 70

Size

Total Lines 70
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 14.1344

Importance

Changes 7
Bugs 4 Features 1
Metric Value
cc 14
eloc 39
c 7
b 4
f 1
nc 70
nop 1
dl 0
loc 70
ccs 31
cts 34
cp 0.9118
crap 14.1344
rs 6.2666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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