Skip to content

Commit f457713

Browse files
committed
Containerize sokil databases
The main focus of this change is to make those optional dependencies more testable. Unfortunately, some phpstan-ignores had to be included, since ::set is not a PsrContainer method. We're only using it on tests though, so it's fine. It targets our php-di container for testing purposes only. The real implementation only relies on ::get. This change also has the side effect of improving the performance of those validators by not instantiating their databases each time a iso validator is built, achieving massive improvements in those scenarios. A small benchmark with no assertions was added to track that improvement.
1 parent 0381718 commit f457713

File tree

13 files changed

+265
-38
lines changed

13 files changed

+265
-38
lines changed

src/ContainerRegistry.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,13 @@
3838
use Respect\Validation\Message\Renderer;
3939
use Respect\Validation\Transformers\Prefix;
4040
use Respect\Validation\Transformers\Transformer;
41+
use Sokil\IsoCodes\Database\Countries;
42+
use Sokil\IsoCodes\Database\Currencies;
43+
use Sokil\IsoCodes\Database\Languages;
44+
use Sokil\IsoCodes\Database\Subdivisions;
4145
use Symfony\Contracts\Translation\TranslatorInterface;
4246

47+
use function class_exists;
4348
use function DI\autowire;
4449
use function DI\create;
4550
use function DI\factory;
@@ -51,6 +56,21 @@ final class ContainerRegistry
5156
/** @param array<string, mixed> $definitions */
5257
public static function createContainer(array $definitions = []): Container
5358
{
59+
foreach (
60+
[
61+
Countries::class,
62+
Subdivisions::class,
63+
Languages::class,
64+
Currencies::class,
65+
] as $requiredClass
66+
) {
67+
if (!class_exists($requiredClass)) {
68+
continue;
69+
}
70+
71+
$definitions[$requiredClass] = autowire($requiredClass);
72+
}
73+
5474
return new Container($definitions + [
5575
Transformer::class => create(Prefix::class),
5676
TemplateResolver::class => create(TemplateResolver::class),
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
/*
4+
* SPDX-License-Identifier: MIT
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Respect\Validation\Exceptions;
11+
12+
use Psr\Container\NotFoundExceptionInterface;
13+
14+
final class MissingClassException extends ComponentException implements Exception, NotFoundExceptionInterface
15+
{
16+
}

src/Validators/CountryCode.php

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@
1818
namespace Respect\Validation\Validators;
1919

2020
use Attribute;
21+
use Psr\Container\NotFoundExceptionInterface;
22+
use Respect\Validation\ContainerRegistry;
2123
use Respect\Validation\Exceptions\InvalidValidatorException;
2224
use Respect\Validation\Exceptions\MissingComposerDependencyException;
2325
use Respect\Validation\Message\Template;
2426
use Respect\Validation\Result;
2527
use Respect\Validation\Validator;
2628
use Sokil\IsoCodes\Database\Countries;
2729

28-
use function class_exists;
2930
use function in_array;
3031
use function is_string;
3132

@@ -43,14 +44,6 @@ public function __construct(
4344
private string $set = 'alpha-2',
4445
Countries|null $countries = null,
4546
) {
46-
if (!class_exists(Countries::class)) {
47-
throw new MissingComposerDependencyException(
48-
'SubdivisionCode rule requires PHP ISO Codes',
49-
'sokil/php-isocodes',
50-
'sokil/php-isocodes-db-only',
51-
);
52-
}
53-
5447
$availableOptions = ['alpha-2', 'alpha-3', 'numeric'];
5548
if (!in_array($set, $availableOptions, true)) {
5649
throw new InvalidValidatorException(
@@ -60,7 +53,15 @@ public function __construct(
6053
);
6154
}
6255

63-
$this->countries = $countries ?? new Countries();
56+
try {
57+
$this->countries = $countries ?? ContainerRegistry::getContainer()->get(Countries::class);
58+
} catch (NotFoundExceptionInterface) {
59+
throw new MissingComposerDependencyException(
60+
'CountryCode rule requires PHP ISO Codes',
61+
'sokil/php-isocodes',
62+
'sokil/php-isocodes-db-only',
63+
);
64+
}
6465
}
6566

6667
public function evaluate(mixed $input): Result

src/Validators/CurrencyCode.php

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@
1515
namespace Respect\Validation\Validators;
1616

1717
use Attribute;
18+
use Psr\Container\NotFoundExceptionInterface;
19+
use Respect\Validation\ContainerRegistry;
1820
use Respect\Validation\Exceptions\InvalidValidatorException;
1921
use Respect\Validation\Exceptions\MissingComposerDependencyException;
2022
use Respect\Validation\Message\Template;
2123
use Respect\Validation\Result;
2224
use Respect\Validation\Validator;
2325
use Sokil\IsoCodes\Database\Currencies;
2426

25-
use function class_exists;
2627
use function in_array;
2728

2829
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
@@ -39,14 +40,6 @@ public function __construct(
3940
private string $set = 'alpha-3',
4041
Currencies|null $currencies = null,
4142
) {
42-
if (!class_exists(Currencies::class)) {
43-
throw new MissingComposerDependencyException(
44-
'CurrencyCode rule requires PHP ISO Codes',
45-
'sokil/php-isocodes',
46-
'sokil/php-isocodes-db-only',
47-
);
48-
}
49-
5043
$availableSets = ['alpha-3', 'numeric'];
5144
if (!in_array($set, $availableSets, true)) {
5245
throw new InvalidValidatorException(
@@ -56,7 +49,15 @@ public function __construct(
5649
);
5750
}
5851

59-
$this->currencies = $currencies ?? new Currencies();
52+
try {
53+
$this->currencies = $currencies ?? ContainerRegistry::getContainer()->get(Currencies::class);
54+
} catch (NotFoundExceptionInterface) {
55+
throw new MissingComposerDependencyException(
56+
'CurrencyCode rule requires PHP ISO Codes',
57+
'sokil/php-isocodes',
58+
'sokil/php-isocodes-db-only',
59+
);
60+
}
6061
}
6162

6263
public function evaluate(mixed $input): Result

src/Validators/LanguageCode.php

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@
1515
namespace Respect\Validation\Validators;
1616

1717
use Attribute;
18+
use Psr\Container\NotFoundExceptionInterface;
19+
use Respect\Validation\ContainerRegistry;
1820
use Respect\Validation\Exceptions\InvalidValidatorException;
1921
use Respect\Validation\Exceptions\MissingComposerDependencyException;
2022
use Respect\Validation\Message\Template;
2123
use Respect\Validation\Result;
2224
use Respect\Validation\Validator;
23-
use Sokil\IsoCodes\Database\Countries;
2425
use Sokil\IsoCodes\Database\Languages;
2526

26-
use function class_exists;
2727
use function in_array;
2828
use function is_string;
2929

@@ -41,14 +41,6 @@ public function __construct(
4141
private readonly string $set = 'alpha-2',
4242
Languages|null $languages = null,
4343
) {
44-
if (!class_exists(Countries::class)) {
45-
throw new MissingComposerDependencyException(
46-
'LanguageCode rule requires PHP ISO Codes',
47-
'sokil/php-isocodes',
48-
'sokil/php-isocodes-db-only',
49-
);
50-
}
51-
5244
$availableSets = ['alpha-2', 'alpha-3'];
5345
if (!in_array($set, $availableSets, true)) {
5446
throw new InvalidValidatorException(
@@ -58,7 +50,15 @@ public function __construct(
5850
);
5951
}
6052

61-
$this->languages = $languages ?? new Languages();
53+
try {
54+
$this->languages = $languages ?? ContainerRegistry::getContainer()->get(Languages::class);
55+
} catch (NotFoundExceptionInterface) {
56+
throw new MissingComposerDependencyException(
57+
'LanguageCode rule requires PHP ISO Codes',
58+
'sokil/php-isocodes',
59+
'sokil/php-isocodes-db-only',
60+
);
61+
}
6262
}
6363

6464
public function evaluate(mixed $input): Result

src/Validators/Phone.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
use Attribute;
2222
use libphonenumber\NumberParseException;
2323
use libphonenumber\PhoneNumberUtil;
24+
use Psr\Container\NotFoundExceptionInterface;
25+
use Respect\Validation\ContainerRegistry;
2426
use Respect\Validation\Exceptions\InvalidValidatorException;
2527
use Respect\Validation\Exceptions\MissingComposerDependencyException;
2628
use Respect\Validation\Message\Template;
@@ -64,15 +66,16 @@ public function __construct(string|null $countryCode = null, Countries|null $cou
6466
return;
6567
}
6668

67-
if (!class_exists(Countries::class)) {
69+
try {
70+
$countries ??= ContainerRegistry::getContainer()->get(Countries::class);
71+
} catch (NotFoundExceptionInterface) {
6872
throw new MissingComposerDependencyException(
6973
'Phone rule with country code requires PHP ISO Codes',
7074
'sokil/php-isocodes',
7175
'sokil/php-isocodes-db-only',
7276
);
7377
}
7478

75-
$countries ??= new Countries();
7679
$this->country = $countries->getByAlpha2($countryCode);
7780
if ($this->country === null) {
7881
throw new InvalidValidatorException('Invalid country code %s', $countryCode);

src/Validators/SubdivisionCode.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
namespace Respect\Validation\Validators;
1313

1414
use Attribute;
15+
use Psr\Container\NotFoundExceptionInterface;
16+
use Respect\Validation\ContainerRegistry;
1517
use Respect\Validation\Exceptions\InvalidValidatorException;
1618
use Respect\Validation\Exceptions\MissingComposerDependencyException;
1719
use Respect\Validation\Helpers\CanValidateUndefined;
@@ -21,8 +23,6 @@
2123
use Sokil\IsoCodes\Database\Countries;
2224
use Sokil\IsoCodes\Database\Subdivisions;
2325

24-
use function class_exists;
25-
2626
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
2727
#[Template(
2828
'{{subject}} must be a subdivision code of {{countryName|trans}}',
@@ -41,22 +41,24 @@ public function __construct(
4141
Countries|null $countries = null,
4242
Subdivisions|null $subdivisions = null,
4343
) {
44-
if (!class_exists(Countries::class) || !class_exists(Subdivisions::class)) {
44+
try {
45+
$container = ContainerRegistry::getContainer();
46+
$countries ??= $container->get(Countries::class);
47+
$this->subdivisions = $subdivisions ?? $container->get(Subdivisions::class);
48+
} catch (NotFoundExceptionInterface) {
4549
throw new MissingComposerDependencyException(
4650
'SubdivisionCode rule requires PHP ISO Codes',
4751
'sokil/php-isocodes',
4852
'sokil/php-isocodes-db-only',
4953
);
5054
}
5155

52-
$countries ??= new Countries();
5356
$country = $countries->getByAlpha2($countryCode);
5457
if ($country === null) {
5558
throw new InvalidValidatorException('"%s" is not a supported country code', $countryCode);
5659
}
5760

5861
$this->country = $country;
59-
$this->subdivisions = $subdivisions ?? new Subdivisions();
6062
}
6163

6264
public function evaluate(mixed $input): Result

tests/benchmark/IsoCodesBench.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/*
4+
* SPDX-License-Identifier: MIT
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Respect\Validation\Benchmarks;
11+
12+
use PhpBench\Attributes as Bench;
13+
use Respect\Validation\Test\SmokeTestProvider;
14+
use Respect\Validation\ValidatorBuilder;
15+
16+
class IsoCodesBench
17+
{
18+
use SmokeTestProvider;
19+
20+
#[Bench\Iterations(10)]
21+
#[Bench\RetryThreshold(5)]
22+
#[Bench\Revs(5)]
23+
#[Bench\Warmup(1)]
24+
#[Bench\Subject]
25+
public function subdivisionCode(): void
26+
{
27+
ValidatorBuilder::subdivisionCode('US')->evaluate('CA');
28+
}
29+
30+
#[Bench\Iterations(10)]
31+
#[Bench\RetryThreshold(5)]
32+
#[Bench\Revs(5)]
33+
#[Bench\Warmup(1)]
34+
#[Bench\Subject]
35+
public function countryCode(): void
36+
{
37+
ValidatorBuilder::countryCode()->evaluate('US');
38+
}
39+
40+
#[Bench\Iterations(10)]
41+
#[Bench\RetryThreshold(5)]
42+
#[Bench\Revs(5)]
43+
#[Bench\Warmup(1)]
44+
#[Bench\Subject]
45+
public function currencyCode(): void
46+
{
47+
ValidatorBuilder::currencyCode()->evaluate('USD');
48+
}
49+
50+
#[Bench\Iterations(10)]
51+
#[Bench\RetryThreshold(5)]
52+
#[Bench\Revs(5)]
53+
#[Bench\Warmup(1)]
54+
#[Bench\Subject]
55+
public function languageCode(): void
56+
{
57+
ValidatorBuilder::languageCode()->evaluate('en');
58+
}
59+
60+
#[Bench\Iterations(10)]
61+
#[Bench\RetryThreshold(5)]
62+
#[Bench\Revs(5)]
63+
#[Bench\Warmup(1)]
64+
#[Bench\Subject]
65+
public function phone(): void
66+
{
67+
ValidatorBuilder::phone('US')->evaluate('+1 202-555-0125');
68+
}
69+
}

tests/unit/Validators/CountryCodeTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414

1515
namespace Respect\Validation\Validators;
1616

17+
use DI;
1718
use PHPUnit\Framework\Attributes\CoversClass;
1819
use PHPUnit\Framework\Attributes\Group;
1920
use PHPUnit\Framework\Attributes\Test;
21+
use Respect\Validation\ContainerRegistry;
2022
use Respect\Validation\Exceptions\InvalidValidatorException;
23+
use Respect\Validation\Exceptions\MissingComposerDependencyException;
2124
use Respect\Validation\Test\RuleTestCase;
2225

2326
#[Group('validator')]
@@ -36,6 +39,24 @@ public function itShouldThrowsExceptionWhenInvalidFormat(): void
3639
new CountryCode('whatever');
3740
}
3841

42+
#[Test]
43+
public function shouldThrowWhenMissingComponent(): void
44+
{
45+
$mainContainer = ContainerRegistry::getContainer();
46+
ContainerRegistry::setContainer((new DI\ContainerBuilder())->useAutowiring(false)->build());
47+
try {
48+
new CountryCode('alpha-3');
49+
$this->fail('Expected MissingComposerDependencyException was not thrown.');
50+
} catch (MissingComposerDependencyException $e) {
51+
$this->assertStringContainsString(
52+
'CountryCode rule requires PHP ISO Codes',
53+
$e->getMessage(),
54+
);
55+
} finally {
56+
ContainerRegistry::setContainer($mainContainer);
57+
}
58+
}
59+
3960
/** @return iterable<array{CountryCode, mixed}> */
4061
public static function providerForValidInput(): iterable
4162
{

0 commit comments

Comments
 (0)