Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
104 / 104
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
Validator
100.00% covered (success)
100.00%
104 / 104
100.00% covered (success)
100.00%
9 / 9
37
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 new
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 arrayDefine
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 defineRoutine
100.00% covered (success)
100.00%
84 / 84
100.00% covered (success)
100.00%
1 / 1
26
 isUndefinedArrayKey
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getArrayPathStr
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 keyValidate
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 valueValidate
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * @license MIT
5 * @author hazuki3417<hazuki3417@gmail.com>
6 * @copyright 2022 hazuki3417 all rights reserved.
7 */
8
9namespace Selen\Schema;
10
11use Selen\Data\ArrayPath;
12use Selen\Schema\Validate\ArrayDefine;
13use Selen\Schema\Validate\Define;
14use Selen\Schema\Validate\Model\ValidateResult;
15use Selen\Schema\Validate\Model\ValidatorResult;
16use Selen\Schema\Validate\ValueValidateInterface;
17
18class Validator
19{
20    /** @var ArrayDefine */
21    private $arrayDefine;
22
23    /** @var Validate\Model\ValidateResult[] */
24    private $validateResults = [];
25
26    /** @var ArrayPath */
27    private $arrayPath;
28
29    /**
30     * インスタンスを生成します
31     *
32     * @return Validator
33     */
34    private function __construct()
35    {
36        $this->arrayPath = new ArrayPath();
37    }
38
39    /**
40     * インスタンスを生成します
41     */
42    public static function new(): Validator
43    {
44        return new self();
45    }
46
47    /**
48     * key・valueの検証処理を設定します(個別設定)
49     */
50    public function arrayDefine(ArrayDefine $arrayDefine): Validator
51    {
52        $this->arrayDefine = $arrayDefine;
53        return $this;
54    }
55
56    /**
57     * 検証処理を実行します
58     *
59     * @param array<mixed,mixed> $input 変換する配列を渡します
60     */
61    public function execute(array $input): ValidatorResult
62    {
63        $this->defineRoutine($input, $this->arrayDefine);
64        return new ValidatorResult(...$this->validateResults);
65    }
66
67    /**
68     * 定義した配列形式に変換します(個別設定)
69     *
70     * @param array<mixed,mixed> $input       変換する配列を渡します
71     * @param ArrayDefine        $arrayDefine 変換の定義を渡します
72     */
73    private function defineRoutine(
74        array $input,
75        ArrayDefine $arrayDefine
76    ): void {
77        $this->arrayPath->down();
78
79        /** 検証対象の配列にのみ存在するフィールドを検出する処理 */
80        if ($arrayDefine->assocArrayDefineExists) {
81            $inputKeys     = array_keys($input);
82            $undefinedKeys = array_diff($inputKeys, $arrayDefine->keys);
83
84            foreach ($undefinedKeys as $undefinedKey) {
85                $this->arrayPath->setCurrentPath($undefinedKey);
86                $this->validateResults[] = new ValidateResult(
87                    false,
88                    $this->getArrayPathStr(),
89                    'Undefined key.'
90                );
91            }
92        }
93
94        /** @var Define $define */
95        foreach ($arrayDefine->defines as $define) {
96            if ($define->isAssocArrayDefine()) {
97                $this->arrayPath->setCurrentPath($define->key->getName());
98            }
99
100            if ($define->isIndexArrayDefine()) {
101                $this->arrayPath->setCurrentPath('[]');
102            }
103
104            if ($define->isKeyValidate()) {
105                // keyの検証処理
106                $validateResult = $this->keyValidate($define, $input);
107
108                if (!$validateResult->getResult()) {
109                    // 失敗したときのみ結果を保持する
110                    $this->validateResults[] = $validateResult;
111                    continue;
112                }
113            }
114
115            if ($this->isUndefinedArrayKey($define, $input)) {
116                // keyの検証が不要で、input側にkeyがないときに実行される処理
117                continue;
118            }
119
120            if ($define->isValueValidate()) {
121                // valueの検証処理
122
123                /**
124                 * TODO: バリデーションの処理順を統一する。x軸を型定義、y軸を配列の要素名として説明(2次元配列をイメージ)
125                 *       isAssocArrayDefine(): x軸を先に処理してから、y軸を処理する
126                 *       isIndexArrayDefine(): y軸を処理してから、x軸を処理する
127                 */
128                // 値バリデーションのループ(値のバリデーションは複数指定可能)
129                if ($define->isAssocArrayDefine()) {
130                    foreach ($define->valueValidateExecutes as $execute) {
131                        // 連想配列のときの値バリデーション処理
132                        $validateResult = $this->valueValidate($execute, $input[$define->key->getName()]);
133
134                        if (!$validateResult->getResult()) {
135                            // 検証結果が不合格の場合は控えている検証処理は実行しない
136                            // 失敗したときのみ結果を保持する
137                            $this->validateResults[] = $validateResult;
138                            break;
139                        }
140                        // 検証結果が合格の場合は控えている検証処理を実行する。
141                        continue;
142                    }
143                }
144
145                if ($define->isIndexArrayDefine()) {
146                    foreach ($define->valueValidateExecutes as $execute) {
147                        // 要素配列のときの値バリデーション処理
148                        $keyValues = $input;
149                        /** @var bool 配列要素すべてのバリデーションが合格ならtrue、それ以外ならfalse */
150                        $oneLoopValidateResult = true;
151
152                        foreach ($keyValues as $key => $value) {
153                            $this->arrayPath->setCurrentPath(\sprintf('[%s]', $key));
154                            $validateResult = $this->valueValidate($execute, $value);
155
156                            if (!$validateResult->getResult()) {
157                                $oneLoopValidateResult   = false;
158                                $this->validateResults[] = $validateResult;
159                            }
160                        }
161
162                        if (!$oneLoopValidateResult) {
163                            // 配列要素のうち1つでもバリデーション違反した場合は、控えている検証処理は実行しない
164                            break;
165                        }
166                        // 配列要素すべてのバリデーションを合格した場合は、控えている検証処理を実行する。
167                        continue;
168                    }
169                }
170            }
171
172            if ($define->nestedTypeDefineExists()) {
173                // ネストされた定義なら再帰処理を行う
174                if ($define->isAssocArrayDefine()) {
175                    $passRecursionInput = $input[$define->key->getName()];
176
177                    // 値が配列以外の場合は値のバリデーションエラーとする。(ネストされた定義 = 配列形式のため)
178                    if (!\is_array($passRecursionInput)) {
179                        // keyは存在するが、値が配列型以外の場合はエラーとする
180                        $this->validateResults[] = new ValidateResult(
181                            false,
182                            $this->getArrayPathStr(),
183                            'Invalid value. Expecting a value of array type.'
184                        );
185                        continue;
186                    }
187
188                    // keyが存在する + 値が配列型なら再帰処理する
189                    $this->defineRoutine(
190                        $passRecursionInput,
191                        $define->arrayDefine
192                    );
193                }
194
195                if ($define->isIndexArrayDefine()) {
196                    if ($input === []) {
197                        // NOTE: index配列 + 空配列のときは値がないときなのでバリデーションしない
198                        continue;
199                    }
200
201                    $items = $input;
202
203                    foreach ($items as $index => $item) {
204                        $path = \sprintf('[%s]', $index);
205                        $this->arrayPath->setCurrentPath($path);
206
207                        if (\is_string($index)) {
208                            // 要素名に文字列が指定されている = 連想配列
209                            $this->validateResults[] = new ValidateResult(
210                                false,
211                                $this->getArrayPathStr(),
212                                'Invalid key. Expecting indexed array type.'
213                            );
214                            continue;
215                        }
216
217                        if (!\is_array($item)) {
218                            $this->validateResults[] = new ValidateResult(
219                                false,
220                                $this->getArrayPathStr(),
221                                'Invalid value. Expecting a value of array type.'
222                            );
223                            continue;
224                        }
225                        $this->defineRoutine(
226                            $item,
227                            $define->arrayDefine
228                        );
229                    }
230                }
231            }
232        }
233        $this->arrayPath->up();
234    }
235
236    /**
237     * 定義されたkeyが入力側に存在しないかどうか確認します
238     *
239     * @param Define             $define 定義を指定します
240     * @param array<mixed,mixed> $input  入力側の配列を指定します
241     *
242     * @return bool 存在しない場合はtrueを、それ以外の場合はfalseを返します
243     */
244    private function isUndefinedArrayKey(Define $define, array $input): bool
245    {
246        // NOTE: 定義側のkey名はnullを許容していない。nullのときはindex array定義なので検証は必要
247        if ($define->key->getName() === null) {
248            return false;
249        }
250        // 定義したkeyがinput側に存在しない場合、要素参照するとUndefinedが発生するため検証は不要
251        return !array_key_exists($define->key->getName(), $input);
252    }
253
254    /**
255     * 配列の階層パス文字列を取得します
256     *
257     * @return string 配列の階層パス文字列を返します
258     */
259    private function getArrayPathStr(): string
260    {
261        return ArrayPath::toString($this->arrayPath->getPaths());
262    }
263
264    /**
265     * keyの検証処理を行います
266     *
267     * @param Define $define
268     * @param mixed  $value
269     */
270    private function keyValidate($define, $value): ValidateResult
271    {
272        $validateResult = new ValidateResult(true, $this->getArrayPathStr());
273        return \array_key_exists($define->key->getName(), $value) ?
274            $validateResult :
275            $validateResult
276                ->setResult(false)
277                ->setMessage('key is required.');
278    }
279
280    /**
281     * 値の検証処理を行います
282     *
283     * @param Validate\ValueValidateInterface|callable $execute
284     * @param mixed                                    $value
285     */
286    private function valueValidate($execute, $value): ValidateResult
287    {
288        $validateResult = new ValidateResult(true, $this->getArrayPathStr());
289
290        if ($execute instanceof ValueValidateInterface) {
291            return $execute->execute($value, $validateResult);
292        }
293        return \call_user_func($execute, $value, $validateResult);
294    }
295}