Passed
Push — master ( 3b395c...8f2b33 )
by Alexander
03:28 queued 41s
created

Count   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 107
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 19
eloc 32
c 1
b 0
f 0
dl 0
loc 107
ccs 34
cts 34
cp 1
rs 10

3 Methods

Rating   Name   Duplication   Size   Complexity  
B validateValue() 0 30 8
B __construct() 0 60 10
A getOptions() 0 10 1
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 18
    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 18
        if (!$this->min && !$this->max && !$this->exactly) {
0 ignored issues
show
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...
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->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...
70 1
            throw new InvalidArgumentException(
71
                'At least one of these attributes must be specified: $min, $max, $exactly.'
72
            );
73
        }
74
75 17
        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...
76 3
            throw new InvalidArgumentException('$exactly is mutually exclusive with $min and $max.');
77
        }
78
79 14
        if ($this->min && $this->max && $this->min === $this->max) {
0 ignored issues
show
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...
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...
80 1
            throw new InvalidArgumentException('Use $exactly instead.');
81
        }
82
83 13
        parent::__construct(formatter: $formatter, skipOnEmpty: $skipOnEmpty, skipOnError: $skipOnError, when: $when);
84
    }
85
86 19
    protected function validateValue($value, ?ValidationContext $context = null): Result
87
    {
88 19
        $result = new Result();
89
90 19
        if (!is_countable($value)) {
91 6
            $result->addError($this->formatMessage($this->message));
92
93 6
            return $result;
94
        }
95
96 13
        $count = count($value);
97
98 13
        if ($this->exactly !== null && $count !== $this->exactly) {
99 1
            $message = $this->formatMessage($this->notExactlyMessage, ['exactly' => $this->exactly]);
100 1
            $result->addError($message);
101
102 1
            return $result;
103
        }
104
105 12
        if ($this->min !== null && $count < $this->min) {
106 3
            $message = $this->formatMessage($this->tooFewItemsMessage, ['min' => $this->min]);
107 3
            $result->addError($message);
108
        }
109
110 12
        if ($this->max !== null && $count > $this->max) {
111 2
            $message = $this->formatMessage($this->tooManyItemsMessage, ['max' => $this->max]);
112 2
            $result->addError($message);
113
        }
114
115 12
        return $result;
116
    }
117
118 1
    public function getOptions(): array
119
    {
120 1
        return array_merge(parent::getOptions(), [
121 1
            'min' => $this->min,
122 1
            'max' => $this->max,
123 1
            'exactly' => $this->exactly,
124 1
            'message' => $this->formatMessage($this->message),
125 1
            'tooFewItemsMessage' => $this->formatMessage($this->tooFewItemsMessage, ['min' => $this->min]),
126 1
            'tooManyItemsMessage' => $this->formatMessage($this->tooManyItemsMessage, ['max' => $this->max]),
127 1
            'notExactlyMessage' => $this->formatMessage($this->notExactlyMessage, ['exactly' => $this->exactly]),
128
        ]);
129
    }
130
}
131