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

Model::firstField()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 3
eloc 4
c 1
b 0
f 1
nc 3
nop 1
dl 0
loc 8
ccs 4
cts 4
cp 1
crap 3
rs 10
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