Completed
Push — master ( 112076...0ca994 )
by Neomerx
05:14
created

Validator::createRelationshipCaptures()   C

Complexity

Conditions 7
Paths 4

Size

Total Lines 36
Code Lines 28

Duplication

Lines 22
Ratio 61.11 %

Importance

Changes 0
Metric Value
dl 22
loc 36
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 28
nc 4
nop 0
1
<?php namespace Limoncello\Flute\Validation;
2
3
/**
4
 * Copyright 2015-2017 [email protected]
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use Doctrine\DBAL\Types\Type;
20
use Generator;
21
use Limoncello\Contracts\Data\ModelSchemeInfoInterface;
22
use Limoncello\Flute\Contracts\I18n\TranslatorInterface as T;
23
use Limoncello\Flute\Contracts\Schema\JsonSchemesInterface;
24
use Limoncello\Flute\Contracts\Schema\SchemaInterface;
25
use Limoncello\Flute\Contracts\Validation\ValidatorInterface;
26
use Limoncello\Flute\Http\JsonApiResponse;
27
use Limoncello\Flute\Types\DateBaseType;
28
use Limoncello\Validation\Captures\CaptureAggregator;
29
use Limoncello\Validation\Contracts\CaptureAggregatorInterface;
30
use Limoncello\Validation\Contracts\ErrorAggregatorInterface;
31
use Limoncello\Validation\Contracts\RuleInterface;
32
use Limoncello\Validation\Contracts\TranslatorInterface as ValidationTranslatorInterface;
33
use Limoncello\Validation\Errors\ErrorAggregator;
34
use Limoncello\Validation\Validator\Captures;
35
use Limoncello\Validation\Validator\Compares;
36
use Limoncello\Validation\Validator\Converters;
37
use Limoncello\Validation\Validator\ExpressionsX;
38
use Limoncello\Validation\Validator\Generics;
39
use Limoncello\Validation\Validator\Types;
40
use Limoncello\Validation\Validator\Values;
41
use Limoncello\Validation\Validator\Wrappers;
42
use Neomerx\JsonApi\Contracts\Document\DocumentInterface;
43
use Neomerx\JsonApi\Exceptions\JsonApiException;
44
use Psr\Container\ContainerInterface;
45
46
/**
47
 * @package Limoncello\Flute
48
 *
49
 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
50
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
51
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
52
 */
53
class Validator implements ValidatorInterface
54
{
55
    use Captures, Compares, Converters, ExpressionsX, Generics, Types, Values, Wrappers;
56
57
    /** Rule description index */
58
    const RULE_INDEX = 0;
59
60
    /** Rule description index */
61
    const RULE_ATTRIBUTES = self::RULE_INDEX + 1;
62
63
    /** Rule description index */
64
    const RULE_TO_ONE = self::RULE_ATTRIBUTES + 1;
65
66
    /** Rule description index */
67
    const RULE_TO_MANY = self::RULE_TO_ONE + 1;
68
69
    /** Rule description index */
70
    const RULE_UNLISTED_ATTRIBUTE = self::RULE_TO_MANY + 1;
71
72
    /** Rule description index */
73
    const RULE_UNLISTED_RELATIONSHIP = self::RULE_UNLISTED_ATTRIBUTE + 1;
74
75
    /**
76
     * @var ContainerInterface
77
     */
78
    private $container;
79
80
    /**
81
     * @var SchemaInterface|null
82
     */
83
    private $schema = null;
84
85
    /**
86
     * @var string
87
     */
88
    private $jsonType;
89
90
    /**
91
     * @var RuleInterface[]
92
     */
93
    private $rules;
94
95
    /**
96
     * @var int
97
     */
98
    private $errorStatus;
99
100
    /**
101
     * @var null|ErrorCollection
102
     */
103
    private $errorCollection = null;
104
105
    /**
106
     * @var null|CaptureAggregatorInterface
107
     */
108
    private $captureAggregator = null;
109
110
    /**
111
     * @param ContainerInterface $container
112
     * @param string             $jsonType
113
     * @param RuleInterface[]    $rules
114
     * @param int                $errorStatus
115
     */
116
    public function __construct(
117
        ContainerInterface $container,
118
        string $jsonType,
119
        array $rules,
120
        $errorStatus = JsonApiResponse::HTTP_UNPROCESSABLE_ENTITY
121
    ) {
122
        if (array_key_exists(static::RULE_UNLISTED_ATTRIBUTE, $rules) === false) {
123
            $rules[static::RULE_UNLISTED_ATTRIBUTE] = static::fail();
124
        }
125
        if (array_key_exists(static::RULE_UNLISTED_RELATIONSHIP, $rules) === false) {
126
            $rules[static::RULE_UNLISTED_RELATIONSHIP] = static::fail();
127
        }
128
129
        $this->container   = $container;
130
        $this->jsonType    = $jsonType;
131
        $this->rules       = $rules;
132
        $this->errorStatus = $errorStatus;
133
    }
134
135
    /**
136
     * @inheritdoc
137
     */
138
    public function assert(array $jsonData): ValidatorInterface
139
    {
140
        if ($this->check($jsonData) === false) {
141
            throw new JsonApiException($this->getErrors(), $this->getErrorStatus());
142
        }
143
144
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (Limoncello\Flute\Validation\Validator) is incompatible with the return type declared by the interface Limoncello\Flute\Contrac...idatorInterface::assert of type self.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
145
    }
146
147
    /**
148
     * @inheritdoc
149
     */
150
    public function check(array $jsonData): bool
151
    {
152
        $this->resetErrors();
153
        $this->resetCaptureAggregator();
154
155
        $this->validateType($jsonData);
156
        $this->validateId($jsonData);
157
        $this->validateAttributes($jsonData);
158
        $this->validateRelationshipCaptures($jsonData, $this->createRelationshipCaptures());
159
160
        $hasNoErrors = $this->getErrors()->count() <= 0;
161
162
        return $hasNoErrors;
163
    }
164
165
    /**
166
     * @inheritdoc
167
     */
168
    public function getErrors(): ErrorCollection
169
    {
170
        $this->errorCollection !== null ?: $this->resetErrors();
171
172
        return $this->errorCollection;
173
    }
174
175
    /**
176
     * @inheritdoc
177
     */
178
    public function getCaptures(): array
179
    {
180
        $captures = $this->getCaptureAggregator()->getCaptures();
181
182
        return $captures;
183
    }
184
185
    /**
186
     * @return void
187
     */
188
    protected function resetErrors()
189
    {
190
        $this->errorCollection = $this->createErrorCollection();
191
    }
192
193
    /**
194
     * @return ErrorCollection
195
     */
196
    protected function createErrorCollection(): ErrorCollection
197
    {
198
        return new ErrorCollection(
199
            $this->getContainer()->get(T::class),
200
            $this->getContainer()->get(ValidationTranslatorInterface::class),
201
            $this->getErrorStatus()
202
        );
203
    }
204
205
    /**
206
     * @return string
207
     */
208
    protected function getJsonType(): string
209
    {
210
        return $this->jsonType;
211
    }
212
213
    /**
214
     * @return ContainerInterface
215
     */
216
    protected function getContainer(): ContainerInterface
217
    {
218
        return $this->container;
219
    }
220
221
    /**
222
     * @return ModelSchemeInfoInterface
223
     */
224
    protected function getModelSchemes(): ModelSchemeInfoInterface
225
    {
226
        return $this->getContainer()->get(ModelSchemeInfoInterface::class);
227
    }
228
229
    /**
230
     * @return SchemaInterface
231
     */
232
    protected function getSchema(): SchemaInterface
233
    {
234
        if ($this->schema === null) {
235
            /** @var JsonSchemesInterface $jsonSchemes */
236
            $jsonSchemes  = $this->getContainer()->get(JsonSchemesInterface::class);
237
            $this->schema = $jsonSchemes->getSchemaByResourceType($this->getJsonType());
238
        }
239
240
        return $this->schema;
241
    }
242
243
    /**
244
     * @return RuleInterface[]
245
     */
246
    protected function getRules(): array
247
    {
248
        return $this->rules;
249
    }
250
251
    /**
252
     * @return int
253
     */
254
    protected function getErrorStatus(): int
255
    {
256
        return $this->errorStatus;
257
    }
258
259
    /**
260
     * @return CaptureAggregatorInterface
261
     */
262
    protected function createCaptureAggregator(): CaptureAggregatorInterface
263
    {
264
        return new CaptureAggregator();
265
    }
266
267
    /**
268
     * @return ErrorAggregatorInterface
269
     */
270
    protected function createErrorAggregator(): ErrorAggregatorInterface
271
    {
272
        return new ErrorAggregator();
273
    }
274
275
    /**
276
     * @return CaptureAggregatorInterface
277
     */
278
    public function getCaptureAggregator(): CaptureAggregatorInterface
279
    {
280
        $this->captureAggregator !== null ?: $this->resetCaptureAggregator();
281
282
        return $this->captureAggregator;
283
    }
284
285
    /**
286
     * @return void
287
     */
288
    protected function resetCaptureAggregator()
289
    {
290
        $this->captureAggregator = $this->createCaptureAggregator();
291
    }
292
293
    /**
294
     * @param array $jsonData
295
     *
296
     * @return void
297
     */
298
    private function validateType(array $jsonData)
299
    {
300
        $expectedType = $this->getSchema()::TYPE;
301
        $ignoreOthers = static::success();
302
        $rule         = static::arrayX([
303
            DocumentInterface::KEYWORD_DATA => static::arrayX([
304
                DocumentInterface::KEYWORD_TYPE => static::required(static::equals($expectedType)),
305
            ], $ignoreOthers),
306
        ], $ignoreOthers);
307
        foreach ($this->validateRule($rule, $jsonData) as $error) {
308
            $this->getErrors()->addValidationTypeError($error);
309
        }
310
    }
311
312
    /**
313
     * @param RuleInterface $rule
314
     * @param mixed         $input
315
     *
316
     * @return Generator
317
     */
318
    protected function validateRule(RuleInterface $rule, $input): Generator
319
    {
320
        foreach ($rule->validate($input) as $error) {
321
            yield $error;
322
        };
323
324
        $aggregator = $this->createErrorAggregator();
325
        $rule->onFinish($aggregator);
326
327
        foreach ($aggregator->get() as $error) {
328
            yield $error;
329
        }
330
    }
331
332
    /**
333
     * @param array $jsonData
334
     *
335
     * @return void
336
     */
337
    private function validateId(array $jsonData)
338
    {
339
        $idRule = $this->getRules()[static::RULE_INDEX] ?? static::success();
340
        assert($idRule instanceof RuleInterface);
341
342
        // will use primary column name as a capture name for `id`
343
        $captureName  = $this->getModelSchemes()->getPrimaryKey($this->getSchema()::MODEL);
344
        $idRule       = static::singleCapture($captureName, $idRule, $this->getCaptureAggregator());
345
        $ignoreOthers = static::success();
346
        $rule         = static::arrayX([
347
            DocumentInterface::KEYWORD_DATA => static::arrayX([
348
                DocumentInterface::KEYWORD_ID => $idRule,
349
            ], $ignoreOthers)
350
        ], $ignoreOthers);
351
        foreach ($this->validateRule($rule, $jsonData) as $error) {
352
            $this->getErrors()->addValidationIdError($error);
353
        }
354
    }
355
356
    /**
357
     * @param array $jsonData
358
     *
359
     * @return void
360
     */
361
    private function validateAttributes(array $jsonData)
362
    {
363
        $attributeRules     = $this->getRules()[static::RULE_ATTRIBUTES] ?? [];
364
        $schema             = $this->getSchema();
365
        $attributeTypes     = $this->getModelSchemes()->getAttributeTypes($schema::MODEL);
366
        $createTypedCapture = function (string $name, RuleInterface $rule) use ($attributeTypes, $schema) {
367
            $captureName    = $schema->getAttributeMapping($name);
368
            $attributeType  = $attributeTypes[$captureName] ?? Type::STRING;
369
            $untypedCapture = static::singleCapture($captureName, $rule, $this->getCaptureAggregator());
370
            switch ($attributeType) {
371
                case Type::INTEGER:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
372
                    $capture = static::toInt($untypedCapture);
373
                    break;
374
                case Type::FLOAT:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
375
                    $capture = static::toFloat($untypedCapture);
376
                    break;
377
                case Type::BOOLEAN:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
378
                    $capture = static::toBool($untypedCapture);
379
                    break;
380
                case Type::DATE:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
381
                case Type::DATETIME:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
382
                    $capture = static::toDateTime($untypedCapture, DateBaseType::JSON_API_FORMAT);
383
                    break;
384
                default:
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
385
                    $capture = $untypedCapture;
386
                    break;
387
            }
388
389
            return $capture;
390
        };
391
392
        $attributeCaptures = [];
393
        foreach ($attributeRules as $name => $rule) {
0 ignored issues
show
Bug introduced by
The expression $attributeRules of type object<Limoncello\Valida...ts\RuleInterface>|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
394
            assert(is_string($name) === true && empty($name) === false && $rule instanceof RuleInterface);
395
            $attributeCaptures[$name] = $createTypedCapture($name, $rule);
396
        }
397
398
        $attributes   = $jsonData[DocumentInterface::KEYWORD_DATA][DocumentInterface::KEYWORD_ATTRIBUTES] ?? [];
399
        $unlistedRule = $this->getRules()[static::RULE_UNLISTED_ATTRIBUTE] ?? null;
400
        $dataErrors   = $this->validateRule(static::arrayX($attributeCaptures, $unlistedRule), $attributes);
401
402
        foreach ($dataErrors as $error) {
403
            $this->getErrors()->addValidationAttributeError($error);
404
        }
405
    }
406
407
    /**
408
     * @return array
409
     */
410
    private function createRelationshipCaptures(): array
411
    {
412
        $toOneRules   = $this->getRules()[static::RULE_TO_ONE] ?? [];
413
        $toManyRules  = $this->getRules()[static::RULE_TO_MANY] ?? [];
414
        $aggregator   = $this->getCaptureAggregator();
415
        $jsonSchemes  = $this->getContainer()->get(JsonSchemesInterface::class);
416
        $schema       = $this->getSchema();
417
        $modelSchemes = $this->getModelSchemes();
418
        $modelClass   = $schema::MODEL;
419
420
        $relationshipCaptures = [];
421 View Code Duplication
        foreach ($toOneRules as $name => $rule) {
0 ignored issues
show
Bug introduced by
The expression $toOneRules of type object<Limoncello\Valida...ts\RuleInterface>|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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...
422
            assert(is_string($name) === true && empty($name) === false && $rule instanceof RuleInterface);
423
            $modelRelName   = $schema->getRelationshipMapping($name);
424
            $captureName    = $modelSchemes->getForeignKey($modelClass, $modelRelName);
425
            $expectedSchema = $jsonSchemes->getModelRelationshipSchema($modelClass, $modelRelName);
426
            $relationshipCaptures[$name] = $this->createSingleData(
427
                $name,
428
                static::equals($expectedSchema::TYPE),
429
                static::singleCapture($captureName, $rule, $aggregator)
430
            );
431
        }
432 View Code Duplication
        foreach ($toManyRules as $name => $rule) {
0 ignored issues
show
Bug introduced by
The expression $toManyRules of type object<Limoncello\Valida...ts\RuleInterface>|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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...
433
            assert(is_string($name) === true && empty($name) === false && $rule instanceof RuleInterface);
434
            $modelRelName   = $schema->getRelationshipMapping($name);
435
            $expectedSchema = $jsonSchemes->getModelRelationshipSchema($modelClass, $modelRelName);
436
            $captureName    = $modelRelName;
437
            $relationshipCaptures[$name] = $this->createMultiData(
438
                $name,
439
                static::equals($expectedSchema::TYPE),
440
                static::multiCapture($captureName, $rule, $aggregator)
441
            );
442
        }
443
444
        return $relationshipCaptures;
445
    }
446
447
    /**
448
     * @param array $jsonData
449
     * @param array $relationshipCaptures
450
     *
451
     * @return void
452
     */
453
    private function validateRelationshipCaptures(array $jsonData, array $relationshipCaptures)
454
    {
455
        $relationships = $jsonData[DocumentInterface::KEYWORD_DATA][DocumentInterface::KEYWORD_RELATIONSHIPS] ?? [];
456
        $unlistedRule  = $this->getRules()[static::RULE_UNLISTED_RELATIONSHIP] ?? null;
457
        $dataErrors    = $this->validateRule(static::arrayX($relationshipCaptures, $unlistedRule), $relationships);
458
        foreach ($dataErrors as $error) {
459
            $this->getErrors()->addValidationRelationshipError($error);
460
        }
461
    }
462
463
    /**
464
     * @param RuleInterface $typeRule
465
     * @param RuleInterface $idRule
466
     *
467
     * @return RuleInterface
468
     */
469
    private function createOptionalIdentity(RuleInterface $typeRule, RuleInterface $idRule): RuleInterface
470
    {
471
        return self::andX(self::isArray(), self::arrayX([
472
            DocumentInterface::KEYWORD_TYPE => $typeRule,
473
            DocumentInterface::KEYWORD_ID   => $idRule,
474
        ])->disableAutoParameterNames());
475
    }
476
477
    /**
478
     * @param string        $name
479
     * @param RuleInterface $typeRule
480
     * @param RuleInterface $idRule
481
     *
482
     * @return RuleInterface
483
     */
484
    private function createSingleData($name, RuleInterface $typeRule, RuleInterface $idRule): RuleInterface
485
    {
486
        $identityRule  = $this->createOptionalIdentity($typeRule, $idRule);
487
        $nullValueRule = static::andX($idRule, static::isNull());
488
489
        return static::andX(static::isArray(), static::arrayX([
490
            DocumentInterface::KEYWORD_DATA => static::orX($identityRule, $nullValueRule),
491
        ])->disableAutoParameterNames()->setParameterName($name));
492
    }
493
494
    /**
495
     * @param string        $name
496
     * @param RuleInterface $typeRule
497
     * @param RuleInterface $idRule
498
     *
499
     * @return RuleInterface
500
     */
501
    private function createMultiData(string $name, RuleInterface $typeRule, RuleInterface $idRule): RuleInterface
502
    {
503
        $identityRule = $this->createOptionalIdentity($typeRule, $idRule);
504
505
        return static::andX(static::isArray(), static::arrayX([
506
            DocumentInterface::KEYWORD_DATA => static::andX(static::isArray(), static::eachX($identityRule)),
507
        ])->disableAutoParameterNames()->setParameterName($name));
508
    }
509
}
510