Completed
Push — master ( fbe049...cd378b )
by Ori
07:11
created

BaseField::castValue()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 12
nc 5
nop 1
dl 0
loc 20
rs 8.8571
c 1
b 0
f 0
1
<?php
2
3
namespace frictionlessdata\tableschema\Fields;
4
5
use frictionlessdata\tableschema\Exceptions\FieldValidationException;
6
use frictionlessdata\tableschema\SchemaValidationError;
7
8
abstract class BaseField
9
{
10
    public function __construct($descriptor = null)
11
    {
12
        $this->descriptor = empty($descriptor) ? (object) [] : $descriptor;
13
    }
14
15
    public function descriptor()
16
    {
17
        return $this->descriptor;
18
    }
19
20
    public function fullDescriptor()
21
    {
22
        $fullDescriptor = $this->descriptor();
23
        $fullDescriptor->format = $this->format();
24
        $fullDescriptor->type = $this->type();
25
26
        return $fullDescriptor;
27
    }
28
29
    public function name()
30
    {
31
        return $this->descriptor()->name;
32
    }
33
34
    public function format()
35
    {
36
        return isset($this->descriptor()->format) ? $this->descriptor()->format : 'default';
37
    }
38
39
    public function constraints()
40
    {
41
        if (!$this->constraintsDisabled && isset($this->descriptor()->constraints)) {
42
            return $this->descriptor()->constraints;
43
        } else {
44
            return (object) [];
45
        }
46
    }
47
48
    public function required()
49
    {
50
        return isset($this->constraints()->required) && $this->constraints()->required;
51
    }
52
53
    public function unique()
54
    {
55
        return isset($this->constraints()->unique) && $this->constraints()->unique;
56
    }
57
58
    public function disableConstraints()
59
    {
60
        $this->constraintsDisabled = true;
61
62
        return $this;
63
    }
64
65
    public function enum()
66
    {
67
        if (isset($this->constraints()->enum) && !empty($this->constraints()->enum)) {
68
            return $this->constraints()->enum;
69
        } else {
70
            return [];
71
        }
72
    }
73
74
    /**
75
     * try to create a field object based on the descriptor
76
     * by default uses the type attribute
77
     * return the created field object or false if the descriptor does not match this field.
78
     *
79
     * @param object $descriptor
80
     *
81
     * @return bool|BaseField
82
     */
83
    public static function inferDescriptor($descriptor)
84
    {
85
        if (isset($descriptor->type) && $descriptor->type == static::type()) {
86
            return new static($descriptor);
87
        } else {
88
            return false;
89
        }
90
    }
91
92
    /**
93
     * try to create a new field object based on the given value.
94
     *
95
     * @param mixed       $val
96
     * @param null|object $descriptor
97
     * @param bool @lenient
98
     *
99
     * @return bool|BaseField
100
     */
101
    public static function infer($val, $descriptor = null, $lenient = false)
102
    {
103
        $field = new static($descriptor);
104
        try {
105
            $field->castValue($val);
106
        } catch (FieldValidationException $e) {
107
            return false;
108
        }
109
        $field->inferProperties($val, $lenient);
110
111
        return $field;
112
    }
113
114
    public function inferProperties($val, $lenient)
115
    {
116
        // should be implemented by extending classes
117
        // allows adding / modfiying descriptor properties based on the given value
118
        $this->descriptor->type = $this->type();
119
    }
120
121
    /**
122
     * @param mixed $val
123
     *
124
     * @return mixed
125
     *
126
     * @throws \frictionlessdata\tableschema\Exceptions\FieldValidationException;
127
     */
128
    final public function castValue($val)
129
    {
130
        if ($this->isEmptyValue($val)) {
131
            if ($this->required()) {
132
                throw $this->getValidationException('field is required', $val);
133
            }
134
135
            return null;
136
        } else {
137
            $val = $this->validateCastValue($val);
138
            if (!$this->constraintsDisabled) {
139
                $validationErrors = $this->checkConstraints($val);
140
                if (count($validationErrors) > 0) {
141
                    throw new FieldValidationException($validationErrors);
142
                }
143
            }
144
145
            return $val;
146
        }
147
    }
148
149
    public function validateValue($val)
150
    {
151
        try {
152
            $this->castValue($val);
153
154
            return [];
155
        } catch (FieldValidationException $e) {
156
            return $e->validationErrors;
157
        }
158
    }
159
160
    /**
161
     * get a unique identifier for this field
162
     * used in the inferring process
163
     * this is usually the type, but can be modified to support more advanced inferring process.
164
     *
165
     * @param bool @lenient
166
     *
167
     * @return string
168
     */
169
    public function getInferIdentifier($lenient = false)
170
    {
171
        return $this->type();
172
    }
173
174
    /**
175
     * should be implemented by extending classes to return the table schema type of this field.
176
     *
177
     * @return string
178
     */
179
    public static function type()
180
    {
181
        throw new \Exception('must be implemented by extending classes');
182
    }
183
184
    protected $descriptor;
185
    protected $constraintsDisabled = false;
186
187
    protected function getValidationException($errorMsg = null, $val = null)
188
    {
189
        return new FieldValidationException([
190
            new SchemaValidationError(SchemaValidationError::FIELD_VALIDATION, [
191
                'field' => isset($this->descriptor()->name) ? $this->name() : 'unknown',
192
                'value' => $val,
193
                'error' => is_null($errorMsg) ? 'invalid value' : $errorMsg,
194
            ]),
195
        ]);
196
    }
197
198
    protected function isEmptyValue($val)
199
    {
200
        return is_null($val);
201
    }
202
203
    /**
204
     * @param mixed $val
205
     *
206
     * @return mixed
207
     *
208
     * @throws \frictionlessdata\tableschema\Exceptions\FieldValidationException;
209
     */
210
    // extending classes should extend this method
211
    // value is guaranteed not to be an empty value, that is handled elsewhere
212
    // should raise FieldValidationException on any validation errors
213
    // can use getValidationException function to get a simple exception with single validation error message
214
    // you can also throw an exception with multiple validation errors manually
215
    abstract protected function validateCastValue($val);
216
217
    protected function checkConstraints($val)
218
    {
219
        $validationErrors = [];
220
        $allowedValues = $this->getAllowedValues();
221
        if (!empty($allowedValues) && !$this->checkAllowedValues($allowedValues, $val)) {
222
            $validationErrors[] = new SchemaValidationError(SchemaValidationError::FIELD_VALIDATION, [
223
                'field' => $this->name(),
224
                'value' => $val,
225
                'error' => 'value not in enum',
226
            ]);
227
        }
228
        $constraints = $this->constraints();
229 View Code Duplication
        if (isset($constraints->pattern)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
230
            if (!$this->checkPatternConstraint($val, $constraints->pattern)) {
231
                $validationErrors[] = new SchemaValidationError(SchemaValidationError::FIELD_VALIDATION, [
232
                    'field' => $this->name(),
233
                    'value' => $val,
234
                    'error' => 'value does not match pattern',
235
                ]);
236
            }
237
        }
238 View Code Duplication
        if (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
239
            isset($constraints->minimum)
240
            && !$this->checkMinimumConstraint($val, $this->castValueNoConstraints($constraints->minimum))
241
        ) {
242
            $validationErrors[] = new SchemaValidationError(SchemaValidationError::FIELD_VALIDATION, [
243
                'field' => $this->name(),
244
                'value' => $val,
245
                'error' => 'value is below minimum',
246
            ]);
247
        }
248 View Code Duplication
        if (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
249
            isset($constraints->maximum)
250
            && !$this->checkMaximumConstraint($val, $this->castValueNoConstraints($constraints->maximum))
251
        ) {
252
            $validationErrors[] = new SchemaValidationError(SchemaValidationError::FIELD_VALIDATION, [
253
                'field' => $this->name(),
254
                'value' => $val,
255
                'error' => 'value is above maximum',
256
            ]);
257
        }
258 View Code Duplication
        if (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
259
            isset($constraints->minLength) && !$this->checkMinLengthConstraint($val, $constraints->minLength)
260
        ) {
261
            $validationErrors[] = new SchemaValidationError(SchemaValidationError::FIELD_VALIDATION, [
262
                'field' => $this->name(),
263
                'value' => $val,
264
                'error' => 'value is below minimum length',
265
            ]);
266
        }
267 View Code Duplication
        if (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
268
            isset($constraints->maxLength) && !$this->checkMaxLengthConstraint($val, $constraints->maxLength)
269
        ) {
270
            $validationErrors[] = new SchemaValidationError(SchemaValidationError::FIELD_VALIDATION, [
271
                'field' => $this->name(),
272
                'value' => $val,
273
                'error' => 'value is above maximum length',
274
            ]);
275
        }
276
277
        return $validationErrors;
278
    }
279
280
    protected function checkPatternConstraint($val, $pattern)
281
    {
282
        return preg_match('/^'.$pattern.'$/', $val) === 1;
283
    }
284
285
    protected function checkMinimumConstraint($val, $minConstraint)
286
    {
287
        return $val >= $minConstraint;
288
    }
289
290
    protected function checkMaximumConstraint($val, $maxConstraint)
291
    {
292
        return $val <= $maxConstraint;
293
    }
294
295
    protected function checkMinLengthConstraint($val, $minLength)
296
    {
297
        if (is_string($val)) {
298
            return strlen($val) >= $minLength;
299
        } elseif (is_array($val)) {
300
            return count($val) >= $minLength;
301
        } elseif (is_object($val)) {
302
            return count($val) >= $minLength;
303
        } else {
304
            throw $this->getValidationException('invalid value for minLength constraint', $val);
305
        }
306
    }
307
308
    protected function checkMaxLengthConstraint($val, $maxLength)
309
    {
310
        if (is_string($val)) {
311
            return strlen($val) <= $maxLength;
312
        } elseif (is_array($val)) {
313
            return count($val) <= $maxLength;
314
        } elseif (is_object($val)) {
315
            return count($val) <= $maxLength;
316
        } else {
317
            throw $this->getValidationException('invalid value for maxLength constraint', $val);
318
        }
319
    }
320
321
    protected function getAllowedValues()
322
    {
323
        $allowedValues = [];
324
        foreach ($this->enum() as $val) {
325
            $allowedValues[] = $this->castValueNoConstraints($val);
326
        }
327
328
        return $allowedValues;
329
    }
330
331
    protected function checkAllowedValues($allowedValues, $val)
332
    {
333
        return in_array($val, $allowedValues, !is_object($val));
334
    }
335
336
    protected function castValueNoConstraints($val)
337
    {
338
        $this->disableConstraints();
339
        $val = $this->castValue($val);
340
        $this->constraintsDisabled = false;
341
342
        return $val;
343
    }
344
}
345