Passed
Push — master ( f5421b...da5af7 )
by Bruno
06:35
created

Model   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 260
Duplicated Lines 0 %

Test Coverage

Coverage 53.85%

Importance

Changes 7
Bugs 3 Features 1
Metric Value
eloc 95
c 7
b 3
f 1
dl 0
loc 260
ccs 56
cts 104
cp 0.5385
rs 9.2
wmc 40

15 Methods

Rating   Name   Duplication   Size   Complexity  
A fromStruct() 0 5 1
A getName() 0 3 1
A __construct() 0 3 1
A getFields() 0 3 1
A fromJSON() 0 9 2
A getData() 0 3 1
A fromJSONFile() 0 7 2
A viewable() 0 3 1
A getRandom() 0 7 2
A serialize() 0 17 1
A toJSON() 0 7 2
A parseStruct() 0 11 4
D validate() 0 67 18
A editable() 0 3 1
A getDefault() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like Model often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Model, and based on these observations, apply Extract Interface, too.

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
    /**
24
     * Model data being processed.
25
     * @var array
26
     */
27
    protected $_data;
28
29
    /**
30
     *
31
     * @param string $name
32
     * @throws Exception
33
     */
34 9
    protected function __construct(string $name = '')
35
    {
36 9
        $this->name = $name;
37 9
    }
38
39
    /**
40
     * Loads model from JSON file.
41
     *
42
     * @param array $struct
43
     * @return Model
44
     */
45 5
    public static function fromStruct(array $struct): Model
46
    {
47 5
        $m = new self('');
48 5
        $m->parseStruct($struct);
49 5
        return $m;
50
    }
51
52
    /**
53
     * Loads model from JSON file.
54
     *
55
     * @param string $name The JSON filename.
56
     * @return Model
57
     */
58
    public static function fromJSONFile(string $name): Model
59
    {
60
        $json = file_get_contents($name); // TODO: path
61
        if ($json === false) {
62
            throw new Exception('File not found');
63
        }
64
        return static::fromJSON($json);
65
    }
66
67
    /**
68
     * Loads model from JSON string
69
     *
70
     * @param string $json The JSON string.
71
     * @return Model
72
     */
73 5
    public static function fromJSON(string $json): Model
74
    {
75 5
        $data = \json_decode($json, true);
76 5
        if ($data === null) {
77 1
            throw new Exception('Invalid JSON format');
78
        }
79 4
        $m = new self('');
80 4
        $m->parseStruct($data);
81 1
        return $m;
82
    }
83
84 1
    public function getName(): string
85
    {
86 1
        return $this->name;
87
    }
88
89 1
    public function getFields(): array
90
    {
91 1
        return $this->fields;
92
    }
93
94 2
    public function getData(): array
95
    {
96 2
        return $this->_data;
97
    }
98
99
    /**
100
     * Validates a set of data against this model.
101
     *
102
     * @param array $data A field name => data array.
103
     * @return array
104
     */
105 4
    public function validate(array $data): array
106
    {
107 4
        $this->_data = $data;
108 4
        $validate = [];
109 4
        $errors = [];
110 4
        foreach ($data as $name => $d) {
111
            // expected?
112 4
            if (!array_key_exists($name, $this->fields)) {
113
                $errors[$name] = "Field $name does not exist in this model";
114
                continue;
115
            }
116
117 4
            $field = $this->fields[$name];
118
119
            // must be filled?
120 4
            if ($field->getValidators()[Datatype::FILLED] ?? false) {
121 1
                if (empty($d)) {
122 1
                    $errors[$name] = "Field $name is empty";
123 1
                    continue;
124
                }
125
            }
126
127 4
            if ($field->getValidators()[Datatype::REQUIRED_WITH] ?? false) {
128
                $expectedFields = $field->getValidators()[Datatype::REQUIRED_WITH];
129
                $found = false;
130
                foreach ($expectedFields as $ef) {
131
                    if (array_key_exists($data, $ef)) {
132
                        $found = true;
133
                    }
134
                }
135
                if (!$found) {
136
                    $errors[$name] = "Field $name is required when at least one of fields " . join(',', $expectedFields . ' are present');
0 ignored issues
show
Bug introduced by
$expectedFields . ' are present' of type string is incompatible with the type array expected by parameter $pieces of join(). ( Ignorable by Annotation )

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

136
                    $errors[$name] = "Field $name is required when at least one of fields " . join(',', /** @scrutinizer ignore-type */ $expectedFields . ' are present');
Loading history...
137
                    continue;
138
                }
139
            }
140
141 4
            if ($field->getValidators()[Datatype::REQUIRED_WITH_ALL] ?? false) {
142
                $expectedFields = $field->getValidators()[Datatype::REQUIRED_WITH_ALL];
143
                $found = true;
144
                foreach ($expectedFields as $ef) {
145
                    if (!array_key_exists($data, $ef)) {
146
                        $found = false;
147
                        break;
148
                    }
149
                }
150
                if (!$found) {
151
                    $errors[$name] = "Field $name is required when at all fields " . join(',', $expectedFields . ' are present');
152
                    continue;
153
                }
154
            }
155
156
            try {
157 4
                $validate[$name] = $field->getDatatype()->validate($d, $field, $this);
158 2
            } catch (Exception $e) {
159 4
                $errors[$name] = $e->getMessage();
160
            }
161
        }
162 4
        foreach ($this->fields as $name => $field) {
163 4
            if (($field->getValidators()[Datatype::REQUIRED] ?? false)
164 4
                && !array_key_exists($name, $validate)
165 4
                && !array_key_exists($name, $errors)
166
            ) {
167 4
                $errors[$name] = "Field $name is missing";
168
            }
169
        }
170 4
        $this->_data = [];
171 4
        return ['validated' => $validate, 'errors' => $errors];
172
    }
173
174
    /**
175
     * Serializes this model to JSON.
176
     *
177
     * @return array
178
     */
179
    public function serialize(): array
180
    {
181
        $fields = array_map(
182
            function ($f) {
183
                return [
184
                    'datatype' => $f->getDatatype()->getName(),
185
                    'validators' => $f->getValidators(),
186
                    'extensions' => $f->getExtensions()
187
                ];
188
            },
189
            $this->fields
190
        );
191
        $model = [
192
            'name' => $this->name,
193
            'fields' => $fields
194
        ];
195
        return $model;
196
    }
197
198
    public function toJSON(): string
199
    {
200
        $t = json_encode($this->serialize());
201
        if (!$t) {
202
            throw new Exception('Cannot serialize');
203
        }
204
        return $t;
205
    }
206
207
    /**
208
     * Renders a readonly view of the model with given data.
209
     *
210
     * @param array $modelData
211
     * @return string
212
     */
213
    public function viewable(array $modelData): string
214
    {
215
        return FrameworkComposer::viewable($this, $modelData);
216
    }
217
218
    /**
219
     * Renders a form view of the model with given data.
220
     *
221
     * @param array $modelData
222
     * @return string
223
     */
224
    public function editable(array $modelData = []): string
225
    {
226
        return FrameworkComposer::editable($this, $modelData);
227
    }
228
229 1
    public function getRandom(): array
230
    {
231 1
        $data = [];
232 1
        foreach ($this->fields as $f) {
233 1
            $data[$f->getName()] = $f->getDatatype()->getRandom();
234
        }
235 1
        return $data;
236
    }
237
238
    /**
239
     * Returns an array with the default values of each field
240
     *
241
     * @return array Field name => value
242
     */
243
    public function getDefault(): array
244
    {
245
        $data = [];
246
        foreach ($this->fields as $f) {
247
            $data[$f->getName()] = $f->getDatatype()->getDefault();
248
        }
249
        return $data;
250
    }
251
252
    /**
253
     * Parses struct
254
     *
255
     * @param array $data
256
     * @throws Exception
257
     * @return void
258
     */
259 9
    protected function parseStruct(array $data)
260
    {
261 9
        if (!array_key_exists('name', $data)) {
262 1
            throw new Exception('Missing name in model');
263
        }
264 8
        if (!array_key_exists('fields', $data)) {
265 1
            throw new Exception('Missing fields in model');
266
        }
267 7
        $this->name = $data['name'];
268 7
        foreach ($data['fields'] as $fieldName => $fieldData) {
269 7
            $this->fields[$fieldName] = Field::getFromData($fieldName, $fieldData);
270
        }
271 6
    }
272
}
273