Passed
Push — master ( 951dea...0f2edd )
by Bruno
10:24
created

Model::getRenderable()   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 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
ccs 0
cts 2
cp 0
crap 2
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
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) {
0 ignored issues
show
introduced by
The condition $restrictFields === null is always false.
Loading history...
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
    /**
322 1
     * Renders a readonly view of the model with given data.
323
     *
324
     * @param FrameworkComposer $composer
325
     * @param array $modelData Actual data for the fields to render. Can be empty.
326
     * @param string[]|callable $restrictFields If present, restrict rendered fields. Can either
327
     * be an array of strings (field names) or a callback which is called for each field.
328
     * Callable signature: (Field $field, Model $m, array $modelData): boolean
329
     * @return HTMLNode[]
330
     */
331
    public function viewableNodes(FrameworkComposer $composer, array $modelData, $restrictFields = null): array
332
    {
333
        $this->_data = $modelData;
334
        $this->_restrictFields = $restrictFields;
335
        $r = $composer->viewableNodes($this, $modelData, $restrictFields);
0 ignored issues
show
Unused Code introduced by
The call to Formularium\FrameworkComposer::viewableNodes() has too many arguments starting with $restrictFields. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

335
        /** @scrutinizer ignore-call */ 
336
        $r = $composer->viewableNodes($this, $modelData, $restrictFields);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
336
        $this->_data = [];
337
        $this->_restrictFields = null;
338
        return $r;
339
    }
340
341
    /**
342
     * Renders a readonly view of the model with given data.
343
     *
344
     * @param FrameworkComposer $composer
345
     * @param array $modelData Actual data for the fields to render. Can be empty.
346 37
     * @param string[]|callable $restrictFields If present, restrict rendered fields. Can either
347
     * be an array of strings (field names) or a callback which is called for each field.
348 37
     * Callable signature: (Field $field, Model $m, array $modelData): boolean
349 1
     * @return string
350
     */
351 36
    public function viewable(FrameworkComposer $composer, array $modelData, $restrictFields = null): string
352 1
    {
353
        $this->_data = $modelData;
354 35
        $this->_restrictFields = $restrictFields;
355 35
        $r = $composer->viewable($this, $modelData, $restrictFields);
0 ignored issues
show
Unused Code introduced by
The call to Formularium\FrameworkComposer::viewable() has too many arguments starting with $restrictFields. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

355
        /** @scrutinizer ignore-call */ 
356
        $r = $composer->viewable($this, $modelData, $restrictFields);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
356 35
        $this->_data = [];
357
        $this->_restrictFields = null;
358 34
        return $r;
359
    }
360
361
    /**
362
     * Renders a form view of the model with given data.
363
     *
364 34
     * @param FrameworkComposer $composer
365
     * @param array $modelData
366
     * @param string[]|callable $restrictFields If present, restrict rendered fields. Can either
367
     * be an array of strings (field names) or a callback which is called for each field.
368
     * Callable signature: (Field $field, Model $m, array $modelData): boolean
369
     * @return HTMLNode[]
370
     */
371
    public function editableNodes(FrameworkComposer $composer, array $modelData = [], $restrictFields = null): array
372
    {
373
        $this->_data = $modelData;
374
        $this->_restrictFields = $restrictFields;
375
        $r = $composer->editableNodes($this, $modelData, $restrictFields);
0 ignored issues
show
Unused Code introduced by
The call to Formularium\FrameworkComposer::editableNodes() has too many arguments starting with $restrictFields. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

375
        /** @scrutinizer ignore-call */ 
376
        $r = $composer->editableNodes($this, $modelData, $restrictFields);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
376
        $this->_data = [];
377
        $this->_restrictFields = null;
378
        return $r;
379
    }
380
381
    /**
382
     * Renders a form view of the model with given data.
383
     *
384
     * @param FrameworkComposer $composer
385
     * @param array $modelData
386
     * @param string[]|callable $restrictFields If present, restrict rendered fields. Can either
387
     * be an array of strings (field names) or a callback which is called for each field.
388
     * Callable signature: (Field $field, Model $m, array $modelData): boolean
389
     * @return string
390
     */
391
    public function editable(FrameworkComposer $composer, array $modelData = [], $restrictFields = null): string
392
    {
393
        $this->_data = $modelData;
394
        $this->_restrictFields = $restrictFields;
395
        $r = $composer->editable($this, $modelData, $restrictFields);
0 ignored issues
show
Unused Code introduced by
The call to Formularium\FrameworkComposer::editable() has too many arguments starting with $restrictFields. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

395
        /** @scrutinizer ignore-call */ 
396
        $r = $composer->editable($this, $modelData, $restrictFields);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
396
        $this->_data = [];
397
        $this->_restrictFields = null;
398
        return $r;
399
    }
400
401
    public function getRandom(): array
402
    {
403
        $data = [];
404
        foreach ($this->fields as $f) {
405
            $data[$f->getName()] = $f->getDatatype()->getRandom();
406
        }
407
        return $data;
408
    }
409
410
    /**
411
     * Returns an array with the default values of each field
412
     *
413
     * @return array Field name => value
414
     */
415
    public function getDefault(): array
416
    {
417
        $data = [];
418
        foreach ($this->fields as $f) {
419
            $data[$f->getName()] = $f->getDatatype()->getDefault();
420
        }
421
        return $data;
422
    }
423
424
    /**
425
     * Parses struct
426
     *
427
     * @param array $data
428
     * @throws Exception
429
     * @return void
430
     */
431
    protected function parseStruct(array $data)
432
    {
433
        if (!array_key_exists('name', $data)) {
434
            throw new Exception('Missing name in model');
435
        }
436
        if (!array_key_exists('fields', $data)) {
437
            throw new Exception('Missing fields in model');
438
        }
439
        $this->name = $data['name'];
440
        foreach ($data['fields'] as $fieldName => $fieldData) {
441
            $this->fields[$fieldName] = Field::getFromData($fieldName, $fieldData);
442
        }
443
        if (array_key_exists('renderable', $data)) {
444
            if (!is_array($data['renderable'])) {
445
                throw new Exception('Model extension must be an array');
446
            }
447
            $this->renderable = $data['renderable'];
448
        }
449
    }
450
}
451