Passed
Push — master ( 0f2edd...d98c91 )
by Bruno
09:06
created

Model::validate()   C

Complexity

Conditions 14
Paths 70

Size

Total Lines 72
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 14.0398

Importance

Changes 7
Bugs 4 Features 1
Metric Value
cc 14
eloc 41
nc 70
nop 1
dl 0
loc 72
ccs 32
cts 34
cp 0.9412
crap 14.0398
rs 6.2666
c 7
b 4
f 1

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
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 toGraphqlTypeDefinition(): string
322 1
    {
323
        $defs = [];
324
        foreach ($this->getFields() as $field) {
325
            $defs[] = $field->toGraphqlTypeDefinition();
326
        }
327
        return 'type ' . $this->getName() . " {\n  " .
328
            str_replace("\n", "\n  ", join("", $defs)) .
329
            "\n}\n\n";
330
    }
331
332
    /**
333
     * Renders a readonly view of the model with given data.
334
     *
335
     * @param FrameworkComposer $composer
336
     * @param array $modelData Actual data for the fields to render. Can be empty.
337
     * @param string[]|callable $restrictFields If present, restrict rendered fields. Can either
338
     * be an array of strings (field names) or a callback which is called for each field.
339
     * Callable signature: (Field $field, Model $m, array $modelData): boolean
340
     * @return HTMLNode[]
341
     */
342
    public function viewableNodes(FrameworkComposer $composer, array $modelData, $restrictFields = null): array
343
    {
344
        $this->_data = $modelData;
345
        $this->_restrictFields = $restrictFields;
346 37
        $r = $composer->viewableNodes($this, $modelData);
347
        $this->_data = [];
348 37
        $this->_restrictFields = null;
349 1
        return $r;
350
    }
351 36
352 1
    /**
353
     * Renders a readonly view of the model with given data.
354 35
     *
355 35
     * @param FrameworkComposer $composer
356 35
     * @param array $modelData Actual data for the fields to render. Can be empty.
357
     * @param string[]|callable $restrictFields If present, restrict rendered fields. Can either
358 34
     * be an array of strings (field names) or a callback which is called for each field.
359
     * Callable signature: (Field $field, Model $m, array $modelData): boolean
360
     * @return string
361
     */
362
    public function viewable(FrameworkComposer $composer, array $modelData, $restrictFields = null): string
363
    {
364 34
        $this->_data = $modelData;
365
        $this->_restrictFields = $restrictFields;
366
        $r = $composer->viewable($this, $modelData);
367
        $this->_data = [];
368
        $this->_restrictFields = null;
369
        return $r;
370
    }
371
372
    /**
373
     * Renders a form view of the model with given data.
374
     *
375
     * @param FrameworkComposer $composer
376
     * @param array $modelData
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, array $modelData): boolean
380
     * @return HTMLNode[]
381
     */
382
    public function editableNodes(FrameworkComposer $composer, array $modelData = [], $restrictFields = null): array
383
    {
384
        $this->_data = $modelData;
385
        $this->_restrictFields = $restrictFields;
386
        $r = $composer->editableNodes($this, $modelData);
387
        $this->_data = [];
388
        $this->_restrictFields = null;
389
        return $r;
390
    }
391
392
    /**
393
     * Renders a form view of the model with given data.
394
     *
395
     * @param FrameworkComposer $composer
396
     * @param array $modelData
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
    public function editable(FrameworkComposer $composer, array $modelData = [], $restrictFields = null): string
403
    {
404
        $this->_data = $modelData;
405
        $this->_restrictFields = $restrictFields;
406
        $r = $composer->editable($this, $modelData);
407
        $this->_data = [];
408
        $this->_restrictFields = null;
409
        return $r;
410
    }
411
412
    /**
413
     * Generates random data for this model
414
     *
415
     * @return array An associative array field name => data.
416
     */
417
    public function getRandom(): array
418
    {
419
        $data = [];
420
        foreach ($this->getFields() as $f) {
421
            $data[$f->getName()] = $f->getDatatype()->getRandom();
422
        }
423
        return $data;
424
    }
425
426
    /**
427
     * Returns an array with the default values of each field
428
     *
429
     * @return array Field name => value
430
     */
431
    public function getDefault(): array
432
    {
433
        $data = [];
434
        foreach ($this->getFields() as $f) {
435
            $data[$f->getName()] = $f->getDatatype()->getDefault();
436
        }
437
        return $data;
438
    }
439
440
    /**
441
     * Parses struct
442
     *
443
     * @param array $data
444
     * @throws Exception
445
     * @return void
446
     */
447
    protected function parseStruct(array $data)
448
    {
449
        if (!array_key_exists('name', $data)) {
450
            throw new Exception('Missing name in model');
451
        }
452
        if (!array_key_exists('fields', $data)) {
453
            throw new Exception('Missing fields in model');
454
        }
455
        $this->name = $data['name'];
456
        foreach ($data['fields'] as $fieldName => $fieldData) {
457
            $this->fields[$fieldName] = Field::getFromData($fieldName, $fieldData);
458
        }
459
        if (array_key_exists('renderable', $data)) {
460
            if (!is_array($data['renderable'])) {
461
                throw new Exception('Model extension must be an array');
462
            }
463
            $this->renderable = $data['renderable'];
464
        }
465
    }
466
}
467