Test Failed
Push — master ( 235b12...b53b80 )
by Bruno
08:13
created

Model::appendRenderable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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