-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathDataSet.php
504 lines (439 loc) · 12.8 KB
/
DataSet.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
<?php
declare(strict_types=1);
namespace Upmind\ProvisionBase\Provider\DataSet;
use ArrayAccess;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Support\Facades\Validator as ValidatorFactory;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Validation\ValidationException;
use JsonSerializable;
use Upmind\ProvisionBase\Exception\InvalidDataSetException;
/**
* DTO encapsulating a data set. If the data set is invalid according to the
* rules returned by static::rules(), an `InvalidDataSetException` will be thrown
* for any attempt to take data from it.
*
* @phpstan-consistent-constructor
*/
abstract class DataSet implements ArrayAccess, JsonSerializable, Arrayable, Jsonable
{
/**
* Input values with nested data sets expanded.
*
* @var array $values
*/
protected $values;
/**
* Raw input data.
*
* @var array $rawValues
*/
protected $rawValues;
/**
* Data set rules.
*
* @var Rules
*/
protected $rules;
/**
* @var \Illuminate\Contracts\Validation\Validator|null
*/
protected $validator;
/**
* Whether or not the data set has yet been validated.
*
* @var bool
*/
protected $isValidated = false;
/**
* Whether or not auto-validation is enabled for this data set instance.
*
* @var bool
*/
protected $validationEnabled = true;
/**
* Returns an array of laravel validation rules for this data set.
*
* @link https://laravel.com/docs/5.8/validation#available-validation-rules
*
* @return Rules<array<string[]>>
*/
abstract public static function rules(): Rules;
/**
* Instantiate the data set with the given values.
*
* @param mixed[] $values Raw data
* @param bool $autoValidation Enable or disable auto-validation of this data set instance
*/
public function __construct($values = [], bool $autoValidation = true)
{
$this->values = $this->rawValues = (array)$this->recursiveToArray($values); // convert to raw array(s)
$this->rules = static::rules();
$this->validationEnabled = $autoValidation;
$this->fillNestedDataSets(); // cast values to data sets if appropriate
}
/**
* Set/update a value on the data set, with automatic casting for nested data
* sets.
*
* @param string $key Data set key
* @param mixed $value Data set value
*/
protected function setValue(string $key, $value): void
{
$value = $this->recursiveToArray($value); // convert to raw array(s)
$this->rawValues[$key] = $value;
$this->values[$key] = $this->castValue($key, $value); //cast to data set if appropriate
$this->validator = null;
$this->isValidated = false;
}
/**
* Instantiate the data set with the given values.
*
* @param mixed[] $values Raw data
* @param bool $validationEnabled Enable or disable validation of this data set instance
*
* @return static
*/
public static function create($values = [], bool $validationEnabled = true)
{
return new static($values, $validationEnabled);
}
/**
* Get a value from the data set by key.
*
* @param string $key Key of the desired value
* @param mixed $default Default value to return if the given key does not exist
*
* @return mixed|null
*/
public function get($key, $default = null)
{
$this->autoValidate();
return $this->has($key)
? $this->values[$key]
: value($default);
}
/**
* Determine whether the data set contains the given value by key.
*
* @param string $key Key of the value to be checked
*
* @return bool
*/
public function has($key): bool
{
return array_key_exists($key, $this->values);
}
/**
* Get all values from the data set, with nested data sets expanded.
*
* @return mixed[]
*/
public function all(): array
{
$this->autoValidate();
return $this->values;
}
/**
* Get the raw values used to instantiate the data set.
*
* @return mixed[]
*/
public function raw(): array
{
$this->autoValidate();
return $this->rawValues;
}
/**
* Get or toggle "auto-validation". If auto-validation is enabled, the DataSet
* will validate itself the first time values are accessed.
*
* @param bool|null $enabled Pass bool to set, or null to get
*
* @return bool Whether or not the DataSet will auto-validate
*/
public function autoValidation(?bool $enabled = null): bool
{
if (isset($enabled)) {
$this->validationEnabled = $enabled;
}
return $this->validationEnabled;
}
/**
* Get or set this instance's "auto-validation" toggle.
*
* @deprecated Use `DataSet::autoValidation()` instead
*
* @param bool|null $enabled Pass true or false to enable/disable, or null to return current setting
*
* @return bool Whether auto-validation is enabled or not
*/
public function validationEnabled(?bool $enabled = null): bool
{
return $this->autoValidation($enabled);
}
/**
* Validate the data set against its validation rules.
*
* @throws InvalidDataSetException If the data set is invalid
*/
public function validate(): void
{
try {
$this->validator()->validate();
$this->isValidated = true;
} catch (ValidationException $e) {
$this->isValidated = false;
throw new InvalidDataSetException($e);
}
}
/**
* Determine whether this data set has been validated.
*/
public function isValidated(): bool
{
return $this->isValidated;
}
/**
* Validate the data set if it has not yet been validated.
*
* @throws InvalidDataSetException If the data set is invalid
*/
public function validateIfNotYetValidated(): void
{
if (!$this->isValidated()) {
$this->validate();
}
}
/**
* Get an array of validation errors for this instance's data.
*
* @return array<string[]>
*/
public function errors(): array
{
try {
$this->validate();
return [];
} catch (InvalidDataSetException $e) {
return $e->errors();
}
}
/**
* Validate the data set if auto-validation is enabled and the data has not yet been validated.
*
* @throws InvalidDataSetException If the data set is invalid
*/
protected function autoValidate(): void
{
if ($this->autoValidation()) {
$this->validateIfNotYetValidated();
}
}
/**
* Get the validator for this data set.
*/
protected function validator(): Validator
{
if (!isset($this->validator)) {
$this->validator = $this->makeValidator($this->rawValues, $this->rules);
}
return $this->validator;
}
/**
* Instantiate and return a new validator for the given data and validation rules.
*
* @param array $data Raw input data
* @param Rules $rules Data set rules
*/
protected function makeValidator($data, Rules $rules): Validator
{
return ValidatorFactory::make($data, $rules->expand());
}
/**
* Replace nested data with nested data sets, according to this data set's
* rules.
*/
protected function fillNestedDataSets(): void
{
foreach ($this->rules->raw() as $field => $rules) {
foreach (RuleParser::explodeRules($rules) as $rule) {
if (RuleParser::isDataSet($rule)) {
if (is_array($data = data_get($this->values, $field))) {
if (RuleParser::fieldIsArray($field)) {
$field = RuleParser::unArrayField($field);
$data = array_map(function ($data) use ($rule) {
return $this->castToDataSet($data, $rule);
}, $data);
} else {
$data = $this->castToDataSet($data, $rule);
}
data_set($this->values, $field, $data);
}
continue 2;
}
}
}
}
/**
* Casts the given value to a data set according to this data set's rules.
*
* @param string $key Value key
* @param mixed $value Value data
*
* @return mixed|DataSet $value The given value, cast to a DataSet if appropriate
*/
protected function castValue(string $key, $value)
{
$keys = [$key, sprintf('%s.*', $key)];
foreach ($keys as $ruleKey) {
foreach (RuleParser::explodeRules($this->rules->raw($ruleKey)) as $rule) {
if (RuleParser::isDataSet($rule) && is_array($value) && !empty($value)) {
if (RuleParser::fieldIsArray($ruleKey)) {
return array_map(function ($value) use ($rule) {
return $this->castToDataSet($value, $rule);
}, $value);
}
return $this->castToDataSet($value, $rule);
}
}
}
return $value;
}
/**
* Cast the given value to the given DataSet.
*
* @param array|DataSet $value Raw data
* @param string $dataSetClass DataSet class
*
* @return DataSet
*/
protected function castToDataSet($data, string $dataSetClass): DataSet
{
return $dataSetClass::create($data, false);
}
/**
* Determine whether the data set contains the given value using array access
* syntax.
*
* @param string $offset
*
* @return bool
*/
public function offsetExists($offset): bool
{
return $this->has($offset);
}
/**
* Get a value from the data set using array access syntax.
*/
public function offsetGet($offset): mixed
{
return $this->get($offset, function () use ($offset) {
if (!array_key_exists($offset, $this->rules->raw())) {
trigger_error(sprintf('Undefined data set index: %s[%s]', get_class($this), $offset), E_USER_NOTICE);
}
return null;
});
}
/**
* Don't allow mutations to the data set from outside.
*/
public function offsetSet($offset, $value): void
{
//
}
/**
* Don't allow mutations to the data set from outside.
*/
public function offsetUnset($offset): void
{
//
}
/**
* Determine whether the data set contains the given value using object
* property access syntax.
*
* @param string $key
*
* @return bool
*/
public function __isset($key): bool
{
return $this->has($key);
}
/**
* Get a value from the data set using object property access syntax.
*
* @return mixed
*/
public function __get($key)
{
return $this->get($key, function () use ($key) {
if (!array_key_exists($key, $this->rules->raw())) {
trigger_error(sprintf('Undefined data set property: %s::$%s', get_class($this), $key), E_USER_NOTICE);
}
return null;
});
}
/**
* Don't allow mutations to the data set from outside.
*/
public function __set($key, $value): void
{
return;
}
/**
* @return array
*/
public function toArray()
{
return $this->raw();
}
/**
* Convert the given value to plain array(s) recursively.
*
* @param mixed $value
*
* @return mixed
*/
protected function recursiveToArray($value)
{
if ($value instanceof DataSet) {
$value = clone $value; // dont interfere with references to this object outside this class
$value->autoValidation(false);
}
if (is_array($value)) {
foreach ($value as $k => $v) {
$value[$k] = $this->recursiveToArray($v);
}
return $value;
}
if ($value instanceof Arrayable) {
return $this->recursiveToArray($value->toArray());
}
return $value;
}
public function jsonSerialize(): array
{
return $this->toArray();
}
/**
* @return string
*/
public function toJson($options = 0)
{
return json_encode($this->jsonSerialize(), $options | JSON_THROW_ON_ERROR);
}
public function __sleep()
{
$this->validator = null;
return array_keys(get_object_vars($this));
}
public function __debugInfo()
{
return $this->values;
}
}