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

Count::getOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 8
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 10
ccs 9
cts 9
cp 1
crap 1
rs 10
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 27
    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 27
        $this->checkLimitsCompatibility();
70 22
        parent::__construct(formatter: $formatter, skipOnEmpty: $skipOnEmpty, skipOnError: $skipOnError, when: $when);
71
    }
72
73 35
    private function checkLimitsCompatibility(): void
74
    {
75 35
        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 34
        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 31
        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 24
    protected function validateValue($value, ?ValidationContext $context = null): Result
124
    {
125 24
        $this->checkLimitsCompatibility();
126
127 19
        $result = new Result();
128
129 19
        if (!is_countable($value)) {
130 6
            $result->addError($this->formatMessage($this->message));
131
132 6
            return $result;
133
        }
134
135 13
        $count = count($value);
136
137 13
        if ($this->exactly !== null && $count !== $this->exactly) {
138 1
            $message = $this->formatMessage($this->notExactlyMessage, ['exactly' => $this->exactly]);
139 1
            $result->addError($message);
140
141 1
            return $result;
142
        }
143
144 12
        if ($this->min !== null && $count < $this->min) {
145 3
            $message = $this->formatMessage($this->tooFewItemsMessage, ['min' => $this->min]);
146 3
            $result->addError($message);
147
        }
148
149 12
        if ($this->max !== null && $count > $this->max) {
150 2
            $message = $this->formatMessage($this->tooManyItemsMessage, ['max' => $this->max]);
151 2
            $result->addError($message);
152
        }
153
154 12
        return $result;
155
    }
156
157 4
    public function getOptions(): array
158
    {
159 4
        return array_merge(parent::getOptions(), [
160 4
            'min' => $this->min,
161 4
            'max' => $this->max,
162 4
            'exactly' => $this->exactly,
163 4
            'message' => $this->formatMessage($this->message),
164 4
            'tooFewItemsMessage' => $this->formatMessage($this->tooFewItemsMessage, ['min' => $this->min]),
165 4
            'tooManyItemsMessage' => $this->formatMessage($this->tooManyItemsMessage, ['max' => $this->max]),
166 4
            'notExactlyMessage' => $this->formatMessage($this->notExactlyMessage, ['exactly' => $this->exactly]),
167
        ]);
168
    }
169
}
170