Passed
Push — master ( a7050c...f62674 )
by Bruno
10:03
created

Model::validate()   C

Complexity

Conditions 14
Paths 70

Size

Total Lines 70
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 15.2919

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
rs 6.2666
ccs 26
cts 32
cp 0.8125
crap 15.2919

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