Skip to content

Commit 3270c1f

Browse files
committed
Make all remaining validators serializable
This commit concludes the effort to make all current validators serializable by fixing the remaining ones. The ability to use `finfo` instances on some filesystem validators was removed. `Image` was refactored to be a readonly class. A command was added to the main `composer qa` flow that checks if all validators are covered by smoke tests (which are currently used by benchmarks and serialization tests). Therefore, this commit also ensures that every validator has a benchmark.
1 parent 66a92cd commit 3270c1f

File tree

10 files changed

+119
-86
lines changed

10 files changed

+119
-86
lines changed

bin/console

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require __DIR__ . '/../vendor/autoload.php';
1212

1313
use Respect\Dev\Commands\CreateMixinCommand;
1414
use Respect\Dev\Commands\DocsLintCommand;
15+
use Respect\Dev\Commands\SmokeTestsCheckCompleteCommand;
1516
use Respect\Dev\Commands\UpdateDomainSuffixesCommand;
1617
use Respect\Dev\Commands\UpdateDomainToplevelCommand;
1718
use Respect\Dev\Commands\UpdatePostalCodesCommand;
@@ -41,6 +42,7 @@ return (static function () {
4142
$application->addCommand(new UpdateDomainSuffixesCommand());
4243
$application->addCommand(new UpdateDomainToplevelCommand());
4344
$application->addCommand(new UpdatePostalCodesCommand());
45+
$application->addCommand(new SmokeTestsCheckCompleteCommand());
4446

4547
return $application->run();
4648
})();

composer.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,22 +73,24 @@
7373
}
7474
},
7575
"scripts": {
76+
"bench-profile": "vendor/bin/phpbench xdebug:profile",
77+
"bench": "vendor/bin/phpbench run",
7678
"docheader": "vendor/bin/docheader check src-dev/ library/ tests/",
77-
"docs": "bin/console docs:lint",
7879
"docs-fix": "bin/console docs:lint --fix",
80+
"docs": "bin/console docs:lint",
81+
"pest": "vendor/bin/pest --testsuite=feature --compact",
7982
"phpcs": "vendor/bin/phpcs",
8083
"phpstan": "vendor/bin/phpstan analyze",
8184
"phpunit": "vendor/bin/phpunit --testsuite=unit",
82-
"pest": "vendor/bin/pest --testsuite=feature --compact",
83-
"bench": "vendor/bin/phpbench run",
84-
"bench:profile": "vendor/bin/phpbench xdebug:profile",
85+
"smoke-complete": "bin/console smoke-tests:check-complete",
8586
"qa": [
8687
"@docheader",
8788
"@phpcs",
8889
"@phpstan",
8990
"@phpunit",
9091
"@pest",
91-
"@docs"
92+
"@docs",
93+
"@smoke-complete"
9294
]
9395
}
9496
}

library/Validators/Image.php

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
use Attribute;
1313
use finfo;
1414
use Respect\Validation\Message\Template;
15-
use Respect\Validation\Validators\Core\Simple;
15+
use Respect\Validation\Result;
16+
use Respect\Validation\Validator;
1617
use SplFileInfo;
1718

1819
use function is_file;
@@ -26,27 +27,22 @@
2627
'{{subject}} must be a valid image file',
2728
'{{subject}} must not be a valid image file',
2829
)]
29-
final class Image extends Simple
30+
final readonly class Image implements Validator
3031
{
31-
public function __construct(
32-
private finfo $fileInfo = new finfo(FILEINFO_MIME_TYPE),
33-
) {
34-
}
35-
36-
public function isValid(mixed $input): bool
32+
public function evaluate(mixed $input): Result
3733
{
3834
if ($input instanceof SplFileInfo) {
39-
return $this->isValid($input->getPathname());
40-
}
41-
42-
if (!is_string($input)) {
43-
return false;
35+
return $this->evaluate($input->getPathname());
4436
}
4537

46-
if (!is_file($input)) {
47-
return false;
38+
if (!is_string($input) || !is_file($input)) {
39+
return Result::failed($input, $this);
4840
}
4941

50-
return mb_strpos((string) $this->fileInfo->file($input), 'image/') === 0;
42+
return Result::of(
43+
mb_strpos((string) (new finfo(FILEINFO_MIME_TYPE))->file($input), 'image/') === 0,
44+
$input,
45+
$this,
46+
);
5147
}
5248
}

library/Validators/Mimetype.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
{
3131
public function __construct(
3232
private string $mimetype,
33-
private finfo $fileInfo = new finfo(),
3433
) {
3534
}
3635

@@ -41,16 +40,13 @@ public function evaluate(mixed $input): Result
4140
}
4241

4342
$parameters = ['mimetype' => $this->mimetype];
44-
if (!is_string($input)) {
45-
return Result::failed($input, $this, $parameters);
46-
}
4743

48-
if (!is_file($input)) {
44+
if (!is_string($input) || !is_file($input)) {
4945
return Result::failed($input, $this, $parameters);
5046
}
5147

5248
return Result::of(
53-
$this->mimetype === $this->fileInfo->file($input, FILEINFO_MIME_TYPE),
49+
$this->mimetype === (new finfo(FILEINFO_MIME_TYPE))->file($input),
5450
$input,
5551
$this,
5652
$parameters,

library/Validators/NoneOf.php

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,8 @@
1212
use Attribute;
1313
use Respect\Validation\Message\Template;
1414
use Respect\Validation\Result;
15-
use Respect\Validation\Validator;
1615
use Respect\Validation\Validators\Core\Composite;
1716

18-
use function array_filter;
19-
use function array_map;
20-
use function array_reduce;
2117
use function count;
2218

2319
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
@@ -38,17 +34,24 @@ final class NoneOf extends Composite
3834

3935
public function evaluate(mixed $input): Result
4036
{
41-
$children = array_map(
42-
static fn(Validator $validator) => $validator->evaluate($input)->withToggledModeAndValidation(),
43-
$this->validators,
44-
);
45-
$valid = array_reduce($children, static fn(bool $carry, Result $result) => $carry && $result->hasPassed, true);
46-
$failed = array_filter($children, static fn(Result $result): bool => !$result->hasPassed);
47-
$template = self::TEMPLATE_SOME;
48-
if (count($children) === count($failed)) {
49-
$template = self::TEMPLATE_ALL;
37+
$failedCount = 0;
38+
$children = [];
39+
foreach ($this->validators as $validator) {
40+
$child = $validator->evaluate($input)->withToggledModeAndValidation();
41+
$children[] = $child;
42+
if ($child->hasPassed) {
43+
continue;
44+
}
45+
46+
$failedCount++;
5047
}
5148

52-
return Result::of($valid, $input, $this, [], $template)->withChildren(...$children);
49+
return Result::of(
50+
$failedCount === 0,
51+
$input,
52+
$this,
53+
[],
54+
count($children) === $failedCount ? self::TEMPLATE_ALL : self::TEMPLATE_SOME,
55+
)->withChildren(...$children);
5356
}
5457
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/*
4+
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
5+
* SPDX-License-Identifier: MIT
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Respect\Dev\Commands;
11+
12+
use Respect\Validation\Test\SmokeTestProvider;
13+
use Symfony\Component\Console\Attribute\AsCommand;
14+
use Symfony\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Input\InputInterface;
16+
use Symfony\Component\Console\Output\OutputInterface;
17+
18+
use function array_diff;
19+
use function array_filter;
20+
use function array_keys;
21+
use function array_map;
22+
use function count;
23+
use function dirname;
24+
use function iterator_to_array;
25+
use function lcfirst;
26+
use function scandir;
27+
use function str_ends_with;
28+
use function substr;
29+
30+
#[AsCommand(
31+
name: 'smoke-tests:check-complete',
32+
description: 'Verifies if all validators are included in the SmokeTestProvider',
33+
)]
34+
final class SmokeTestsCheckCompleteCommand extends Command
35+
{
36+
protected function execute(InputInterface $input, OutputInterface $output): int
37+
{
38+
$validatorDir = dirname(__DIR__, 2) . '/library/Validators';
39+
40+
$missingSmokeTests = array_diff(
41+
array_map(
42+
static fn(string $fileName) => lcfirst(substr($fileName, 0, -4)),
43+
array_filter(
44+
scandir($validatorDir),
45+
static fn(string $fileName) => str_ends_with($fileName, '.php'),
46+
),
47+
),
48+
array_map(
49+
lcfirst(...),
50+
array_keys(
51+
iterator_to_array((new class {
52+
use SmokeTestProvider;
53+
})->provideValidatorInput()),
54+
),
55+
),
56+
);
57+
58+
if (count($missingSmokeTests) > 0) {
59+
$output->writeln('The following validators are missing from the SmokeTestProvider:');
60+
foreach ($missingSmokeTests as $validatorName) {
61+
$output->writeln('- ' . $validatorName);
62+
}
63+
64+
return Command::FAILURE;
65+
}
66+
67+
return Command::SUCCESS;
68+
}
69+
}

tests/feature/SerializableTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Respect\Validation\Test\SmokeTestProvider;
1111

1212
test('Can be serialized and unserialized', function ($validator, $input): void {
13+
set_mock_is_uploaded_file_return(true);
1314
expect(
1415
unserialize(serialize($validator))->validate($input)->isValid(),
1516
)->toBeTrue();

tests/library/SmokeTestProvider.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,10 @@ public static function provideValidatorInput(): Generator
8888
yield 'GreaterThan' => [v::greaterThan(0), 1];
8989
yield 'GreaterThanOrEqual' => [v::greaterThanOrEqual(1), 1];
9090
yield 'Hetu' => [v::hetu(), '010106A9012'];
91-
yield 'HexRGBColor' => [v::hexRgbColor(), '#FFAABB'];
91+
yield 'HexRgbColor' => [v::hexRgbColor(), '#FFAABB'];
9292
yield 'Iban' => [v::iban(), 'SE35 5000 0000 0549 1000 0003'];
9393
yield 'Identical' => [v::identical(123), 123];
94+
yield 'Image' => [v::image(), 'tests/fixtures/valid-image.png'];
9495
yield 'Imei' => [v::imei(), '490154203237518'];
9596
yield 'In' => [v::in(['a', 'b']), 'a'];
9697
yield 'Infinite' => [v::infinite(), INF];
@@ -118,17 +119,20 @@ public static function provideValidatorInput(): Generator
118119
yield 'MacAddress' => [v::macAddress(), '00:11:22:33:44:55'];
119120
yield 'Max' => [v::max(v::equals(30)), [10, 20, 30]];
120121
yield 'Min' => [v::min(v::equals(10)), [10, 20, 30]];
122+
yield 'Mimetype' => [v::mimetype('image/png'), 'tests/fixtures/valid-image.png'];
121123
yield 'Multiple' => [v::multiple(3), 9];
122124
yield 'Named' => [v::named('MyValidator', v::intVal()), 123];
123125
yield 'Negative' => [v::negative(), -1];
124126
yield 'NfeAccessKey' => [v::nfeAccessKey(), '52060433009911002506550120000007800267301615'];
125127
yield 'Nif' => [v::nif(), '12345678Z'];
126128
yield 'Nip' => [v::nip(), '1645865777'];
129+
yield 'NoneOf' => [v::noneOf(v::intVal(), v::floatVal()), 'foo'];
127130
yield 'Not' => [v::not(v::trueVal()), false];
128131
yield 'NullOr' => [v::nullOr(v::intVal()), null];
129132
yield 'Number' => [v::number(), '123'];
130133
yield 'NullType' => [v::nullType(), null];
131134
yield 'NumericVal' => [v::numericVal(), '123'];
135+
yield 'ObjectType' => [v::objectType(), new stdClass()];
132136
yield 'Odd' => [v::odd(), 3];
133137
yield 'OneOf' => [v::oneOf(v::digit(), v::alpha()), 'AB'];
134138
yield 'PerfectSquare' => [v::perfectSquare(), 16];
@@ -152,6 +156,7 @@ public static function provideValidatorInput(): Generator
152156
yield 'ResourceType' => [v::resourceType(), fopen('php://temp', 'r')];
153157
yield 'Roman' => [v::roman(), 'XIV'];
154158
yield 'ScalarVal' => [v::scalarVal(), 'example'];
159+
yield 'Size' => [v::size('KB', v::between(1, 1000)), 'tests/fixtures/valid-image.png'];
155160
yield 'Slug' => [v::slug(), 'a-valid-slug'];
156161
yield 'Sorted' => [v::sorted('ASC'), [1, 2, 3]];
157162
yield 'Space' => [v::space(), " \t\n"];
@@ -162,13 +167,16 @@ public static function provideValidatorInput(): Generator
162167
yield 'SubdivisionCode' => [v::subdivisionCode('US'), 'CA'];
163168
yield 'Subset' => [v::subset(['a', 'b', 'c']), ['a', 'b']];
164169
yield 'SymbolicLink' => [v::symbolicLink(), 'tests/fixtures/symbolic-link'];
170+
yield 'Templated' => [v::templated('Foo', v::stringVal()), 'foo'];
165171
yield 'Time' => [v::time(), '12:34:56'];
166172
yield 'Tld' => [v::tld(), 'com'];
167173
yield 'TrueVal' => [v::trueVal(), true];
168174
yield 'Undef' => [v::undef(), null];
169175
yield 'UndefOr' => [v::undefOr(v::intVal()), null];
170176
yield 'Unique' => [v::unique(), [1, 2, 3]];
177+
yield 'Uploaded' => [v::uploaded(), 'tests/fixtures/valid-image.png'];
171178
yield 'Uppercase' => [v::uppercase(), 'ABC'];
179+
yield 'Url' => [v::url(), 'https://example.com'];
172180
yield 'Uuid' => [v::uuid(), '123e4567-e89b-12d3-a456-426655440000'];
173181
yield 'Version' => [v::version(), '1.2.3'];
174182
yield 'VideoUrl' => [v::videoUrl(), 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'];

tests/unit/Validators/ImageTest.php

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@
99

1010
namespace Respect\Validation\Validators;
1111

12-
use finfo;
1312
use PHPUnit\Framework\Attributes\CoversClass;
1413
use PHPUnit\Framework\Attributes\Group;
15-
use PHPUnit\Framework\Attributes\Test;
1614
use Respect\Validation\Test\RuleTestCase;
1715
use SplFileInfo;
1816
use SplFileObject;
@@ -21,23 +19,6 @@
2119
#[CoversClass(Image::class)]
2220
final class ImageTest extends RuleTestCase
2321
{
24-
#[Test]
25-
public function shouldValidateWithDefinedInstanceOfFileInfo(): void
26-
{
27-
$input = self::fixture('valid-image.gif');
28-
29-
$finfo = $this->createMock(finfo::class);
30-
$finfo
31-
->expects(self::once())
32-
->method('file')
33-
->with($input)
34-
->willReturn('image/gif');
35-
36-
$validator = new Image($finfo);
37-
38-
self::assertValidInput($validator, $input);
39-
}
40-
4122
/** @return iterable<array{Image, mixed}> */
4223
public static function providerForValidInput(): iterable
4324
{

tests/unit/Validators/MimetypeTest.php

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,46 +9,21 @@
99

1010
namespace Respect\Validation\Validators;
1111

12-
use finfo;
1312
use PHPUnit\Framework\Attributes\CoversClass;
1413
use PHPUnit\Framework\Attributes\Group;
15-
use PHPUnit\Framework\Attributes\Test;
1614
use Respect\Validation\Test\RuleTestCase;
1715
use SplFileInfo;
1816
use SplFileObject;
1917

2018
use function random_int;
2119
use function tmpfile;
2220

23-
use const FILEINFO_MIME_TYPE;
2421
use const PHP_INT_MAX;
2522

2623
#[Group('validator')]
2724
#[CoversClass(Mimetype::class)]
2825
final class MimetypeTest extends RuleTestCase
2926
{
30-
#[Test]
31-
public function itShouldValidateWithDefinedFinfoInstance(): void
32-
{
33-
$mimetype = 'application/octet-stream';
34-
$filename = 'tests/fixtures/valid-image.png';
35-
36-
$fileInfoMock = $this
37-
->getMockBuilder(finfo::class)
38-
->disableOriginalConstructor()
39-
->getMock();
40-
41-
$fileInfoMock
42-
->expects(self::once())
43-
->method('file')
44-
->with($filename, FILEINFO_MIME_TYPE)
45-
->willReturn($mimetype);
46-
47-
$validator = new Mimetype($mimetype, $fileInfoMock);
48-
49-
self::assertValidInput($validator, $filename);
50-
}
51-
5227
/** @return iterable<array{Mimetype, mixed}> */
5328
public static function providerForValidInput(): iterable
5429
{

0 commit comments

Comments
 (0)