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

Model::getMetadata()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
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
    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