Passed
Push — master ( a03165...951dea )
by Bruno
07:35
created

Model::editableNodes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 4
c 0
b 0
f 0
nc 1
nop 3
dl 0
loc 6
ccs 0
cts 4
cp 0
crap 2
rs 10
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
     *
35
     * @param string $name
36
     * @throws Exception
37
     */
38 37
    protected function __construct(string $name = '')
39
    {
40 37
        $this->name = $name;
41 37
    }
42
43
    /**
44
     * Loads model from JSON file.
45
     *
46
     * @param array $struct
47
     * @return Model
48
     */
49 33
    public static function fromStruct(array $struct): Model
50
    {
51 33
        $m = new self('');
52 33
        $m->parseStruct($struct);
53 33
        return $m;
54
    }
55
56
    /**
57
     * Loads model from JSON file.
58
     *
59
     * @param string $name The JSON filename.
60
     * @return Model
61
     */
62
    public static function fromJSONFile(string $name): Model
63
    {
64
        $json = file_get_contents($name); // TODO: path
65
        if ($json === false) {
66
            throw new Exception('File not found');
67
        }
68
        return static::fromJSON($json);
69
    }
70
71
    /**
72
     * Loads model from JSON string
73
     *
74
     * @param string $json The JSON string.
75
     * @return Model
76
     */
77 5
    public static function fromJSON(string $json): Model
78
    {
79 5
        $data = \json_decode($json, true);
80 5
        if ($data === null) {
81 1
            throw new Exception('Invalid JSON format');
82
        }
83 4
        $m = new self('');
84 4
        $m->parseStruct($data);
85 1
        return $m;
86
    }
87
88
    /**
89
     * @param string $name
90
     * @param array[]|Field[] $fields
91
     * @return Model
92
     */
93
    public static function create(string $name, array $fields = []): Model
94
    {
95
        $m = new self($name);
96
        foreach ($fields as $fieldName => $fieldData) {
97
            if ($fieldData instanceof Field) {
98
                $m->fields[$fieldData->getName()] = $fieldData;
99
            } else {
100
                $m->fields[$fieldName] = Field::getFromData($fieldName, $fieldData);
101
            }
102
        }
103
        return $m;
104
    }
105
106 1
    public function getName(): string
107
    {
108 1
        return $this->name;
109
    }
110
111 2
    public function getFields(): array
112
    {
113 2
        return $this->fields;
114
    }
115
116 6
    public function getData(): array
117
    {
118 6
        return $this->_data;
119
    }
120
121
    public function getRenderables(): array
122
    {
123
        return $this->renderable;
124
    }
125
126
    /**
127
     * @param string $name
128
     * @param mixed $default
129
     * @return mixed
130
     */
131
    public function getRenderable(string $name, $default)
132
    {
133
        return $this->renderable[$name] ?? $default;
134
    }
135
136 22
    public function getField(string $name): Field
137
    {
138 22
        return $this->fields[$name];
139
    }
140
141
    /**
142
     * filter operation for fields that return true for callable.
143
     *
144
     * @param callable $function
145
     * @return Field[]
146
     */
147 1
    public function filterField(callable $function): array
148
    {
149 1
        return array_filter(
150 1
            $this->fields,
151 1
            $function
152
        );
153
    }
154
155
    public function appendField(Field $f): self
156
    {
157
        $this->fields[$f->getName()] = $f;
158
        return $this;
159
    }
160
161
    /**
162
     * @param Field[] $fields
163
     * @return self
164
     */
165
    public function appendFields(array $fields): self
166
    {
167
        foreach ($fields as $f) {
168
            $this->fields[$f->getName()] = $f;
169
        }
170
        return $this;
171
    }
172
173
    /**
174
     * Validates a set of data against this model.
175
     *
176
     * @param array $data A field name => data array.
177
     * @return array
178
     */
179 8
    public function validate(array $data): array
180
    {
181 8
        $this->_data = $data;
182 8
        $validate = [];
183 8
        $errors = [];
184
185
        // validate data
186 8
        foreach ($data as $name => $d) {
187
            // expected?
188 8
            if (!array_key_exists($name, $this->fields)) {
189 1
                $errors[$name] = "Field $name does not exist in this model";
190 1
                continue;
191
            }
192
193
            // call the datatype validator
194 8
            $field = $this->fields[$name];
195
            try {
196 8
                $validate[$name] = $field->getDatatype()->validate($d, $this);
197
            } catch (Exception $e) {
198
                $errors[$name] = $e->getMessage();
199
            }
200
201
            // call class validators.
202 8
            foreach ($field->getValidators() as $validatorName => $options) {
203
                // special case
204 8
                if ($validatorName === Datatype::REQUIRED) {
205 1
                    continue;
206
                }
207
208
                try {
209 8
                    $validate[$name] = ValidatorFactory::class($validatorName)::validate(
210 8
                        $validate[$name],
211 8
                        $options,
212 8
                        $field->getDatatype(),
213 8
                        $this
214
                    );
215 5
                } catch (Exception $e) {
216 5
                    $errors[$name] = $e->getMessage();
217
                }
218
            }
219
        }
220
221
        // now validate fields, since you may have some that were not in data.
222 8
        foreach ($this->fields as $name => $field) {
223
            // if in field list but not in data
224 8
            if (!array_key_exists($name, $data)) {
225
                // call class validators.
226 5
                foreach ($field->getValidators() as $validatorName => $options) {
227 5
                    if ($validatorName === Datatype::REQUIRED) {
228 1
                        if (!array_key_exists($name, $validate)
229 1
                            && !array_key_exists($name, $errors)
230
                        ) {
231
                            $errors[$name] = "Field $name is missing";
232
                        }
233 1
                        continue;
234
                    }
235
236
                    try {
237 5
                        $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...
238 5
                            null,
239 5
                            $options,
240 5
                            $field->getDatatype(),
241 5
                            $this
242
                        );
243 3
                    } catch (Exception $e) {
244 3
                        $errors[$name] = $e->getMessage();
245
                    }
246
                }
247
            }
248
        }
249 8
        $this->_data = [];
250 8
        return ['validated' => $validate, 'errors' => $errors];
251
    }
252
253
    /**
254
     * Serializes this model to JSON.
255
     *
256
     * @return array
257
     */
258 1
    public function serialize(): array
259
    {
260 1
        $fields = array_map(
261
            function ($f) {
262
                return [
263 1
                    'datatype' => $f->getDatatype()->getName(),
264 1
                    'validators' => $f->getValidators(),
265 1
                    'renderable' => $f->getRenderables()
266
                ];
267 1
            },
268 1
            $this->fields
269
        );
270
        $model = [
271 1
            'name' => $this->name,
272 1
            'fields' => $fields
273
        ];
274 1
        return $model;
275
    }
276
277 1
    public function toJSON(): string
278
    {
279 1
        $t = json_encode($this->serialize());
280 1
        if (!$t) {
281
            throw new Exception('Cannot serialize');
282
        }
283 1
        return $t;
284
    }
285
286
    /**
287
     * Renders a readonly view of the model with given data.
288
     *
289
     * @param FrameworkComposer $composer
290
     * @param array $modelData Actual data for the fields to render. Can be empty.
291
     * @param string[]|callable $restrictFields If present, restrict rendered fields. Can either
292
     * be an array of strings (field names) or a callback which is called for each field.
293 1
     * Callable signature: (Field $field, Model $m, array $modelData): boolean
294
     * @return HTMLNode[]
295 1
     */
296 1
    public function viewableNodes(FrameworkComposer $composer, array $modelData, $restrictFields = null): array
297 1
    {
298 1
        $this->_data = $modelData;
299
        $r = $composer->viewableNodes($this, $modelData, $restrictFields);
300
        $this->_data = [];
301
        return $r;
302
    }
303
304
    /**
305
     * Renders a readonly view of the model with given data.
306
     *
307
     * @param FrameworkComposer $composer
308
     * @param array $modelData Actual data for the fields to render. Can be empty.
309
     * @param string[]|callable $restrictFields If present, restrict rendered fields. Can either
310
     * be an array of strings (field names) or a callback which is called for each field.
311
     * Callable signature: (Field $field, Model $m, array $modelData): boolean
312
     * @return string
313
     */
314
    public function viewable(FrameworkComposer $composer, array $modelData, $restrictFields = null): string
315
    {
316 1
        $this->_data = $modelData;
317
        $r = $composer->viewable($this, $modelData, $restrictFields);
318 1
        $this->_data = [];
319 1
        return $r;
320 1
    }
321
322 1
    /**
323
     * Renders a form view of the model with given data.
324
     *
325
     * @param FrameworkComposer $composer
326
     * @param array $modelData
327
     * @param string[]|callable $restrictFields If present, restrict rendered fields. Can either
328
     * be an array of strings (field names) or a callback which is called for each field.
329
     * Callable signature: (Field $field, Model $m, array $modelData): boolean
330
     * @return HTMLNode[]
331
     */
332
    public function editableNodes(FrameworkComposer $composer, array $modelData = [], $restrictFields = null): array
333
    {
334
        $this->_data = $modelData;
335
        $r = $composer->editableNodes($this, $modelData, $restrictFields);
336
        $this->_data = [];
337
        return $r;
338
    }
339
340
    /**
341
     * Renders a form view of the model with given data.
342
     *
343
     * @param FrameworkComposer $composer
344
     * @param array $modelData
345
     * @param string[]|callable $restrictFields If present, restrict rendered fields. Can either
346 37
     * be an array of strings (field names) or a callback which is called for each field.
347
     * Callable signature: (Field $field, Model $m, array $modelData): boolean
348 37
     * @return string
349 1
     */
350
    public function editable(FrameworkComposer $composer, array $modelData = [], $restrictFields = null): string
351 36
    {
352 1
        $this->_data = $modelData;
353
        $r = $composer->editable($this, $modelData, $restrictFields);
354 35
        $this->_data = [];
355 35
        return $r;
356 35
    }
357
358 34
    public function getRandom(): array
359
    {
360
        $data = [];
361
        foreach ($this->fields as $f) {
362
            $data[$f->getName()] = $f->getDatatype()->getRandom();
363
        }
364 34
        return $data;
365
    }
366
367
    /**
368
     * Returns an array with the default values of each field
369
     *
370
     * @return array Field name => value
371
     */
372
    public function getDefault(): array
373
    {
374
        $data = [];
375
        foreach ($this->fields as $f) {
376
            $data[$f->getName()] = $f->getDatatype()->getDefault();
377
        }
378
        return $data;
379
    }
380
381
    /**
382
     * Parses struct
383
     *
384
     * @param array $data
385
     * @throws Exception
386
     * @return void
387
     */
388
    protected function parseStruct(array $data)
389
    {
390
        if (!array_key_exists('name', $data)) {
391
            throw new Exception('Missing name in model');
392
        }
393
        if (!array_key_exists('fields', $data)) {
394
            throw new Exception('Missing fields in model');
395
        }
396
        $this->name = $data['name'];
397
        foreach ($data['fields'] as $fieldName => $fieldData) {
398
            $this->fields[$fieldName] = Field::getFromData($fieldName, $fieldData);
399
        }
400
        if (array_key_exists('renderable', $data)) {
401
            if (!is_array($data['renderable'])) {
402
                throw new Exception('Model extension must be an array');
403
            }
404
            $this->renderable = $data['renderable'];
405
        }
406
    }
407
}
408