Passed
Push — master ( d6da77...da04b4 )
by Bruno
09:01
created

Model::parseStruct()   B

Complexity

Conditions 8
Paths 16

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

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