Passed
Pull Request — master (#219)
by
unknown
02:48
created

Count::validateValue()   B

Complexity

Conditions 8
Paths 6

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 8

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 17
nc 6
nop 2
dl 0
loc 32
ccs 18
cts 18
cp 1
crap 8
rs 8.4444
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Rule;
6
7
use Attribute;
8
use Countable;
9
use InvalidArgumentException;
10
use Yiisoft\Validator\FormatterInterface;
11
use Yiisoft\Validator\Result;
12
use Yiisoft\Validator\Rule;
13
use Yiisoft\Validator\ValidationContext;
14
15
use function count;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Yiisoft\Validator\Rule\count. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
16
17
/**
18
 * Validates that the value contains certain number of items. Can be applied to arrays or classes implementing
19
 * {@see Countable} interface.
20
 */
21
#[Attribute(Attribute::TARGET_PROPERTY)]
22
final class Count extends Rule
23
{
24 31
    public function __construct(
25
        /**
26
         * @var int|null minimum number of items. null means no minimum number limit.
27
         *
28
         * @see $tooFewItemsMessage for the customized message for a value with too few items.
29
         */
30
        private ?int $min = null,
31
        /**
32
         * @var int|null maximum number of items. null means no maximum number limit.
33
         *
34
         * @see $tooManyItemsMessage for the customized message for a value wuth too many items.
35
         */
36
        private ?int $max = null,
37
        /**
38
         * @var int|null exact number of items. null means no strict comparison. Mutually exclusive with {@see $min} and
39
         * {@see $max}.
40
         */
41
        private ?int $exactly = null,
42
        /**
43
         * @var string user-defined error message used when the value is neither an array nor implementing
44
         * {@see \Countable} interface.
45
         *
46
         * @see Countable
47
         */
48
        private string $message = 'This value must be an array or implement \Countable interface.',
49
        /**
50
         * @var string user-defined error message used when the number of items is smaller than {@see $min}.
51
         */
52
        private string $tooFewItemsMessage = 'This value must contain at least {min, number} ' .
53
        '{min, plural, one{item} other{items}}.',
54
        /**
55
         * @var string user-defined error message used when the number of items is greater than {@see $max}.
56
         */
57
        private string $tooManyItemsMessage = 'This value must contain at most {max, number} ' .
58
        '{max, plural, one{item} other{items}}.',
59
        /**
60
         * @var string user-defined error message used when the number of items does not equal {@see $exactly}.
61
         */
62
        private string $notExactlyMessage = 'This value must contain exactly {max, number} ' .
63
        '{max, plural, one{item} other{items}}.',
64
        ?FormatterInterface $formatter = null,
65
        bool $skipOnEmpty = false,
66
        bool $skipOnError = false,
67
        $when = null
68
    ) {
69 31
        $this->checkLimitsCompatibility();
70 26
        parent::__construct(formatter: $formatter, skipOnEmpty: $skipOnEmpty, skipOnError: $skipOnError, when: $when);
71
    }
72
73 39
    private function checkLimitsCompatibility(): void
74
    {
75 39
        if (!$this->min && !$this->max && !$this->exactly) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->max of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $this->min of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $this->exactly of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
76 2
            throw new InvalidArgumentException(
77
                'At least one of these attributes must be specified: $min, $max, $exactly.'
78
            );
79
        }
80
81 38
        if ($this->exactly && ($this->min || $this->max)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->max of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $this->exactly of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $this->min of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
82 6
            throw new InvalidArgumentException('$exactly is mutually exclusive with $min and $max.');
83
        }
84
85 35
        if ($this->min && $this->max && $this->min === $this->max) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->max of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $this->min of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
86 2
            throw new InvalidArgumentException('Use $exactly instead.');
87
        }
88
    }
89
90
    /**
91
     * @see $min
92
     */
93 5
    public function min(?int $value): self
94
    {
95 5
        $new = clone $this;
96 5
        $new->min = $value;
97
98 5
        return $new;
99
    }
100
101
    /**
102
     * @see $max
103
     */
104 4
    public function max(?int $value): self
105
    {
106 4
        $new = clone $this;
107 4
        $new->max = $value;
108
109 4
        return $new;
110
    }
111
112
    /**
113
     * @see $exactly
114
     */
115 4
    public function exactly(?int $value): self
116
    {
117 4
        $new = clone $this;
118 4
        $new->exactly = $value;
119
120 4
        return $new;
121
    }
122
123
    /**
124
     * @see $message
125
     */
126 1
    public function message(string $value): self
127
    {
128 1
        $new = clone $this;
129 1
        $new->message = $value;
130
131 1
        return $new;
132
    }
133
134
    /**
135
     * @see $tooFewItemsMessage
136
     */
137 1
    public function tooFewItemsMessage(string $value): self
138
    {
139 1
        $new = clone $this;
140 1
        $new->tooFewItemsMessage = $value;
141
142 1
        return $new;
143
    }
144
145
    /**
146
     * @see $tooManyItemsMessage
147
     */
148 1
    public function tooManyItemsMessage(string $value): self
149
    {
150 1
        $new = clone $this;
151 1
        $new->tooManyItemsMessage = $value;
152
153 1
        return $new;
154
    }
155
156
    /**
157
     * @see $notExactlyMessage
158
     */
159 1
    public function notExactlyMessage(string $value): self
160
    {
161 1
        $new = clone $this;
162 1
        $new->notExactlyMessage = $value;
163
164 1
        return $new;
165
    }
166
167 24
    protected function validateValue($value, ?ValidationContext $context = null): Result
168
    {
169 24
        $this->checkLimitsCompatibility();
170
171 19
        $result = new Result();
172
173 19
        if (!is_countable($value)) {
174 6
            $result->addError($this->formatMessage($this->message));
175
176 6
            return $result;
177
        }
178
179 13
        $count = count($value);
180
181 13
        if ($this->exactly !== null && $count !== $this->exactly) {
182 1
            $message = $this->formatMessage($this->notExactlyMessage, ['exactly' => $this->exactly]);
183 1
            $result->addError($message);
184
185 1
            return $result;
186
        }
187
188 12
        if ($this->min !== null && $count < $this->min) {
189 3
            $message = $this->formatMessage($this->tooFewItemsMessage, ['min' => $this->min]);
190 3
            $result->addError($message);
191
        }
192
193 12
        if ($this->max !== null && $count > $this->max) {
194 2
            $message = $this->formatMessage($this->tooManyItemsMessage, ['max' => $this->max]);
195 2
            $result->addError($message);
196
        }
197
198 12
        return $result;
199
    }
200
201 8
    public function getOptions(): array
202
    {
203 8
        return array_merge(parent::getOptions(), [
204 8
            'min' => $this->min,
205 8
            'max' => $this->max,
206 8
            'exactly' => $this->exactly,
207 8
            'message' => $this->formatMessage($this->message),
208 8
            'tooFewItemsMessage' => $this->formatMessage($this->tooFewItemsMessage, ['min' => $this->min]),
209 8
            'tooManyItemsMessage' => $this->formatMessage($this->tooManyItemsMessage, ['max' => $this->max]),
210 8
            'notExactlyMessage' => $this->formatMessage($this->notExactlyMessage, ['exactly' => $this->exactly]),
211
        ]);
212
    }
213
}
214