Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/validators/Contains.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Message template for this validator includes `{{containsValue}}`.
See also:

- [ContainsAny](ContainsAny.md)
- [ContainsCount](ContainsCount.md)
- [EndsWith](EndsWith.md)
- [Equals](Equals.md)
- [Equivalent](Equivalent.md)
Expand Down
1 change: 1 addition & 0 deletions docs/validators/ContainsAny.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,6 @@ See also:

- [AnyOf](AnyOf.md)
- [Contains](Contains.md)
- [ContainsCount](ContainsCount.md)
- [Equivalent](Equivalent.md)
- [In](In.md)
80 changes: 80 additions & 0 deletions docs/validators/ContainsCount.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# ContainsCount

- `ContainsCount(mixed $containsValue, int $count)`
- `ContainsCount(mixed $containsValue, int $count, bool $identical)`

Validates if the input contains a value a specific number of times.

For strings:

```php
v::containsCount('ipsum', 2)->assert('ipsum lorem ipsum');
// Validation passes successfully
```

For arrays:

```php
v::containsCount('ipsum', 2)->assert(['ipsum', 'lorem', 'ipsum']);
// Validation passes successfully
```

A third parameter may be passed for identical comparison instead of equal comparison.

```php
v::containsCount(1, 1, true)->assert([1, 2, 3]);
// Validation passes successfully

v::containsCount('1', 1, true)->assert([1, 2, 3]);
// → `[1, 2, 3]` must contain "1" only once
```

## Templates

### `ContainsCount::TEMPLATE_TIMES`

| Mode | Template |
| ---------- | ---------------------------------------------------------------- |
| `default` | {{subject}} must contain {{containsValue}} {{count}} time(s) |
| `inverted` | {{subject}} must not contain {{containsValue}} {{count}} time(s) |

### `ContainsCount::TEMPLATE_ONCE`

| Mode | Template |
| ---------- | -------------------------------------------------------- |
| `default` | {{subject}} must contain {{containsValue}} only once |
| `inverted` | {{subject}} must not contain {{containsValue}} only once |

## Template placeholders

| Placeholder | Description |
| --------------- | ---------------------------------------------------------------- |
| `containsValue` | The value to search for in the input. |
| `subject` | The validated input or the custom validator name (if specified). |
| `count` | Number of times that the needle might appear in the haystack. |

## Categorization

- Arrays
- Strings

## Changelog

| Version | Description |
| ------: | ----------- |
| 3.0.0 | Created |

---

See also:

- [Contains](Contains.md)
- [ContainsAny](ContainsAny.md)
- [EndsWith](EndsWith.md)
- [Equals](Equals.md)
- [Equivalent](Equivalent.md)
- [Identical](Identical.md)
- [In](In.md)
- [Regex](Regex.md)
- [StartsWith](StartsWith.md)
- [Unique](Unique.md)
3 changes: 3 additions & 0 deletions docs/validators/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ In this page you will find a list of validators by their category.
- [ArrayVal](validators/ArrayVal.md)
- [Contains](validators/Contains.md)
- [ContainsAny](validators/ContainsAny.md)
- [ContainsCount](validators/ContainsCount.md)
- [Each](validators/Each.md)
- [EndsWith](validators/EndsWith.md)
- [In](validators/In.md)
Expand Down Expand Up @@ -228,6 +229,7 @@ In this page you will find a list of validators by their category.
- [Consonant](validators/Consonant.md)
- [Contains](validators/Contains.md)
- [ContainsAny](validators/ContainsAny.md)
- [ContainsCount](validators/ContainsCount.md)
- [Control](validators/Control.md)
- [Digit](validators/Digit.md)
- [Emoji](validators/Emoji.md)
Expand Down Expand Up @@ -331,6 +333,7 @@ In this page you will find a list of validators by their category.
- [Consonant](validators/Consonant.md)
- [Contains](validators/Contains.md)
- [ContainsAny](validators/ContainsAny.md)
- [ContainsCount](validators/ContainsCount.md)
- [Control](validators/Control.md)
- [Countable](validators/Countable.md)
- [CountryCode](validators/CountryCode.md)
Expand Down
2 changes: 2 additions & 0 deletions library/Mixins/AllBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ public static function allContains(mixed $containsValue, bool $identical = false
/** @param non-empty-array<mixed> $needles */
public static function allContainsAny(array $needles, bool $identical = false): Chain;

public static function allContainsCount(mixed $containsValue, int $count, bool $identical = false): Chain;

public static function allControl(string ...$additionalChars): Chain;

public static function allCountable(): Chain;
Expand Down
2 changes: 2 additions & 0 deletions library/Mixins/AllChain.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ public function allContains(mixed $containsValue, bool $identical = false): Chai
/** @param non-empty-array<mixed> $needles */
public function allContainsAny(array $needles, bool $identical = false): Chain;

public function allContainsCount(mixed $containsValue, int $count, bool $identical = false): Chain;

public function allControl(string ...$additionalChars): Chain;

public function allCountable(): Chain;
Expand Down
2 changes: 2 additions & 0 deletions library/Mixins/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ public static function contains(mixed $containsValue, bool $identical = false):
/** @param non-empty-array<mixed> $needles */
public static function containsAny(array $needles, bool $identical = false): Chain;

public static function containsCount(mixed $containsValue, int $count, bool $identical = false): Chain;
Comment thread
henriquemoody marked this conversation as resolved.

public static function control(string ...$additionalChars): Chain;

public static function countable(): Chain;
Expand Down
2 changes: 2 additions & 0 deletions library/Mixins/Chain.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ public function contains(mixed $containsValue, bool $identical = false): Chain;
/** @param non-empty-array<mixed> $needles */
public function containsAny(array $needles, bool $identical = false): Chain;

public function containsCount(mixed $containsValue, int $count, bool $identical = false): Chain;

public function control(string ...$additionalChars): Chain;

public function countable(): Chain;
Expand Down
7 changes: 7 additions & 0 deletions library/Mixins/KeyBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ public static function keyContains(int|string $key, mixed $containsValue, bool $
/** @param non-empty-array<mixed> $needles */
public static function keyContainsAny(int|string $key, array $needles, bool $identical = false): Chain;

public static function keyContainsCount(
int|string $key,
mixed $containsValue,
int $count,
bool $identical = false,
): Chain;

public static function keyControl(int|string $key, string ...$additionalChars): Chain;

public static function keyCountable(int|string $key): Chain;
Expand Down
7 changes: 7 additions & 0 deletions library/Mixins/KeyChain.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ public function keyContains(int|string $key, mixed $containsValue, bool $identic
/** @param non-empty-array<mixed> $needles */
public function keyContainsAny(int|string $key, array $needles, bool $identical = false): Chain;

public function keyContainsCount(
int|string $key,
mixed $containsValue,
int $count,
bool $identical = false,
): Chain;

public function keyControl(int|string $key, string ...$additionalChars): Chain;

public function keyCountable(int|string $key): Chain;
Expand Down
2 changes: 2 additions & 0 deletions library/Mixins/NotBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public static function notContains(mixed $containsValue, bool $identical = false
/** @param non-empty-array<mixed> $needles */
public static function notContainsAny(array $needles, bool $identical = false): Chain;

public static function notContainsCount(mixed $containsValue, int $count, bool $identical = false): Chain;

public static function notControl(string ...$additionalChars): Chain;

public static function notCountable(): Chain;
Expand Down
2 changes: 2 additions & 0 deletions library/Mixins/NotChain.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public function notContains(mixed $containsValue, bool $identical = false): Chai
/** @param non-empty-array<mixed> $needles */
public function notContainsAny(array $needles, bool $identical = false): Chain;

public function notContainsCount(mixed $containsValue, int $count, bool $identical = false): Chain;

public function notControl(string ...$additionalChars): Chain;

public function notCountable(): Chain;
Expand Down
2 changes: 2 additions & 0 deletions library/Mixins/NullOrBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ public static function nullOrContains(mixed $containsValue, bool $identical = fa
/** @param non-empty-array<mixed> $needles */
public static function nullOrContainsAny(array $needles, bool $identical = false): Chain;

public static function nullOrContainsCount(mixed $containsValue, int $count, bool $identical = false): Chain;

public static function nullOrControl(string ...$additionalChars): Chain;

public static function nullOrCountable(): Chain;
Expand Down
2 changes: 2 additions & 0 deletions library/Mixins/NullOrChain.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public function nullOrContains(mixed $containsValue, bool $identical = false): C
/** @param non-empty-array<mixed> $needles */
public function nullOrContainsAny(array $needles, bool $identical = false): Chain;

public function nullOrContainsCount(mixed $containsValue, int $count, bool $identical = false): Chain;

public function nullOrControl(string ...$additionalChars): Chain;

public function nullOrCountable(): Chain;
Expand Down
7 changes: 7 additions & 0 deletions library/Mixins/PropertyBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ public static function propertyContains(
/** @param non-empty-array<mixed> $needles */
public static function propertyContainsAny(string $propertyName, array $needles, bool $identical = false): Chain;

public static function propertyContainsCount(
string $propertyName,
mixed $containsValue,
int $count,
bool $identical = false,
): Chain;

public static function propertyControl(string $propertyName, string ...$additionalChars): Chain;

public static function propertyCountable(string $propertyName): Chain;
Expand Down
7 changes: 7 additions & 0 deletions library/Mixins/PropertyChain.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ public function propertyContains(string $propertyName, mixed $containsValue, boo
/** @param non-empty-array<mixed> $needles */
public function propertyContainsAny(string $propertyName, array $needles, bool $identical = false): Chain;

public function propertyContainsCount(
string $propertyName,
mixed $containsValue,
int $count,
bool $identical = false,
): Chain;

public function propertyControl(string $propertyName, string ...$additionalChars): Chain;

public function propertyCountable(string $propertyName): Chain;
Expand Down
2 changes: 2 additions & 0 deletions library/Mixins/UndefOrBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ public static function undefOrContains(mixed $containsValue, bool $identical = f
/** @param non-empty-array<mixed> $needles */
public static function undefOrContainsAny(array $needles, bool $identical = false): Chain;

public static function undefOrContainsCount(mixed $containsValue, int $count, bool $identical = false): Chain;

public static function undefOrControl(string ...$additionalChars): Chain;

public static function undefOrCountable(): Chain;
Expand Down
2 changes: 2 additions & 0 deletions library/Mixins/UndefOrChain.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ public function undefOrContains(mixed $containsValue, bool $identical = false):
/** @param non-empty-array<mixed> $needles */
public function undefOrContainsAny(array $needles, bool $identical = false): Chain;

public function undefOrContainsCount(mixed $containsValue, int $count, bool $identical = false): Chain;

public function undefOrControl(string ...$additionalChars): Chain;

public function undefOrCountable(): Chain;
Expand Down
106 changes: 106 additions & 0 deletions library/Validators/ContainsCount.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/

declare(strict_types=1);

namespace Respect\Validation\Validators;

use Attribute;
use Respect\Validation\Message\Template;
use Respect\Validation\Result;
use Respect\Validation\Validator;

use function array_reduce;
use function is_array;
use function is_scalar;
use function mb_strtolower;
use function mb_substr_count;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Template(
'{{subject}} must contain {{containsValue}} {{count}} time(s)',
Comment thread
henriquemoody marked this conversation as resolved.
'{{subject}} must not contain {{containsValue}} {{count}} time(s)',
self::TEMPLATE_TIMES,
)]
#[Template(
'{{subject}} must contain {{containsValue}} only once',
'{{subject}} must not contain {{containsValue}} only once',
self::TEMPLATE_ONCE,
)]
final readonly class ContainsCount implements Validator
{
public const string TEMPLATE_TIMES = '__times__';
public const string TEMPLATE_ONCE = '__once__';

public function __construct(
private mixed $containsValue,
private int $count,
private bool $identical = false,
) {
}

public function evaluate(mixed $input): Result
{
$parameters = [
'containsValue' => $this->containsValue,
'count' => $this->count,
];

$template = $this->count === 1 ? self::TEMPLATE_ONCE : self::TEMPLATE_TIMES;

if (is_array($input)) {
return Result::of(
$this->countArrayOccurrences($input) === $this->count,
$input,
$this,
$parameters,
$template,
);
}

if (!is_scalar($input) || !is_scalar($this->containsValue)) {
return Result::failed($input, $this, $parameters, $template);
}

$needle = (string) $this->containsValue;

if ($needle === '') {
return Result::failed($input, $this, $parameters, $template);
}

return Result::of(
$this->countStringOccurrences((string) $input, $needle) === $this->count,
$input,
$this,
$parameters,
$template,
);
}

/** @param array<mixed> $input */
private function countArrayOccurrences(array $input): int
{
return array_reduce(
$input,
$this->identical ? function (int $carry, mixed $item): int {
return $carry + ($item === $this->containsValue ? 1 : 0);
} : function (int $carry, mixed $item): int {
return $carry + ($item == $this->containsValue ? 1 : 0);
},
0,
);
}

private function countStringOccurrences(string $haystack, string $needle): int
{
if ($this->identical) {
return mb_substr_count($haystack, $needle);
}

return mb_substr_count(mb_strtolower($haystack), mb_strtolower($needle));
}
}
5 changes: 1 addition & 4 deletions library/Validators/Domain.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
use function array_pop;
use function count;
use function explode;
use function mb_substr_count;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Template(
Expand Down Expand Up @@ -84,9 +83,7 @@ private function createPartsRule(): Validator
new Not(new StartsWith('-')),
new AnyOf(
new Not(new Contains('--')),
new Callback(static function ($str) {
return mb_substr_count($str, '--') == 1;
}),
new ContainsCount('--', 1),
),
new Not(new EndsWith('-')),
),
Expand Down
Loading