Skip to content

Commit 6075bad

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 b69beb1 commit 6075bad

File tree

13 files changed

+308
-38
lines changed

13 files changed

+308
-38
lines changed

src/ContainerRegistry.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,17 @@
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+
use function sprintf;
4652

4753
final class ContainerRegistry
4854
{
@@ -94,6 +100,10 @@ public static function createContainer(array $definitions = []): Container
94100
),
95101
$container->get(TranslatorInterface::class),
96102
)),
103+
Subdivisions::class => factory(static::optional(Subdivisions::class)),
104+
Countries::class => factory(static::optional(Countries::class)),
105+
Languages::class => factory(static::optional(Languages::class)),
106+
Currencies::class => factory(static::optional(Currencies::class)),
97107
ValidatorBuilder::class => factory(static fn(Container $container) => new ValidatorBuilder(
98108
$container->get(ValidatorFactory::class),
99109
$container->get(Renderer::class),
@@ -119,4 +129,17 @@ public static function setContainer(ContainerInterface $instance): void
119129
{
120130
self::$container = $instance;
121131
}
132+
133+
private static function optional(string $className): callable
134+
{
135+
return static function () use ($className) {
136+
if (!class_exists($className)) {
137+
throw new Exceptions\MissingClassException(
138+
sprintf('Class "%s" is required but not found.', $className),
139+
);
140+
}
141+
142+
return new $className();
143+
};
144+
}
122145
}
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+
}

0 commit comments

Comments
 (0)