Skip to content

Commit 53b3bde

Browse files
committed
Improve performance of Prefix transformer
The Prefix transformer had many loops that could be avoided. This change replaces them for a compiled PCRE regex, taking advantage of recent PCRE JIT capabilities introduced in PHP. These changes offer no performance trade-offs, improving lookup for all categories of prefixes (property/key with shift, ignore list and fallback to simple rule). The most affected is the simple rule (no prefix or no conflict with any kind of prefix rule), yielding the most gains.
1 parent 699f060 commit 53b3bde

2 files changed

Lines changed: 100 additions & 46 deletions

File tree

src/Transformers/Prefix.php

Lines changed: 58 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,75 +4,87 @@
44
* SPDX-License-Identifier: MIT
55
* SPDX-FileCopyrightText: (c) Respect Project Contributors
66
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
7+
* SPDX-FileContributor: Alexandre Gomes Gaigalas <alganet@gmail.com>
78
*/
89

910
declare(strict_types=1);
1011

1112
namespace Respect\Validation\Transformers;
1213

13-
use function array_shift;
14-
use function in_array;
15-
use function str_starts_with;
16-
use function strlen;
17-
use function substr;
14+
use function array_keys;
15+
use function array_slice;
16+
use function implode;
17+
use function preg_match;
18+
use function sprintf;
1819

1920
final class Prefix implements Transformer
2021
{
21-
private const array RULES_TO_SKIP = [
22-
'all',
23-
'allOf',
24-
'key',
25-
'keyExists',
26-
'keyOptional',
27-
'keySet',
28-
'length',
29-
'max',
30-
'maxAge',
31-
'min',
32-
'minAge',
33-
'not',
34-
'emoji',
35-
'nullOr',
36-
'property',
37-
'propertyExists',
38-
'propertyOptional',
39-
'undefOr',
22+
private const array RULES_THAT_PREFIX_OR_STAND_ALONE = [
23+
'all' => true,
24+
'allOf' => true,
25+
'emoji' => true,
26+
'key' => true,
27+
'keyExists' => true,
28+
'keyOptional' => true,
29+
'keySet' => true,
30+
'length' => true,
31+
'max' => true,
32+
'maxAge' => true,
33+
'min' => true,
34+
'minAge' => true,
35+
'not' => true,
36+
'nullOr' => true,
37+
'property' => true,
38+
'propertyExists' => true,
39+
'propertyOptional' => true,
40+
'undefOr' => true,
4041
];
42+
private const array RULES_THAT_USE_SUFFIX_AS_ARGUMENT = [
43+
'key' => true,
44+
'property' => true,
45+
];
46+
47+
private static string|null $regex = null;
4148

4249
public function transform(ValidatorSpec $validatorSpec): ValidatorSpec
4350
{
44-
if ($validatorSpec->wrapper !== null || in_array($validatorSpec->name, self::RULES_TO_SKIP, true)) {
51+
$matches = $this->match($validatorSpec);
52+
if ($matches === []) {
4553
return $validatorSpec;
4654
}
4755

48-
foreach (['all', 'length', 'max', 'min', 'not', 'nullOr', 'undefOr'] as $prefix) {
49-
if (!str_starts_with($validatorSpec->name, $prefix)) {
50-
continue;
51-
}
52-
56+
if (!isset(self::RULES_THAT_USE_SUFFIX_AS_ARGUMENT[$matches['name']])) {
5357
return new ValidatorSpec(
54-
substr($validatorSpec->name, strlen($prefix)),
58+
$matches['rest'],
5559
$validatorSpec->arguments,
56-
new ValidatorSpec($prefix),
60+
new ValidatorSpec($matches['name']),
5761
);
5862
}
5963

60-
foreach (['key', 'property'] as $prefix) {
61-
if (!str_starts_with($validatorSpec->name, $prefix)) {
62-
continue;
63-
}
64-
65-
$arguments = $validatorSpec->arguments;
66-
array_shift($arguments);
67-
$wrapperArguments = [$validatorSpec->arguments[0]];
64+
return new ValidatorSpec(
65+
$matches['rest'],
66+
array_slice($validatorSpec->arguments, 1),
67+
new ValidatorSpec($matches['name'], [$validatorSpec->arguments[0]]),
68+
);
69+
}
6870

69-
return new ValidatorSpec(
70-
substr($validatorSpec->name, strlen($prefix)),
71-
$arguments,
72-
new ValidatorSpec($prefix, $wrapperArguments),
73-
);
71+
/** @return array{}|array{name: string, rest: string} */
72+
private function match(ValidatorSpec $validatorSpec): array
73+
{
74+
if ($validatorSpec->wrapper !== null || isset(self::RULES_THAT_PREFIX_OR_STAND_ALONE[$validatorSpec->name])) {
75+
return [];
7476
}
7577

76-
return $validatorSpec;
78+
preg_match(self::getRegex(), $validatorSpec->name, $matches);
79+
80+
return $matches;
81+
}
82+
83+
private static function getRegex(): string
84+
{
85+
return self::$regex ?? self::$regex = sprintf(
86+
'/^(?<name>%s)(?<rest>.+)$/',
87+
implode('|', array_keys(self::RULES_THAT_PREFIX_OR_STAND_ALONE)),
88+
);
7789
}
7890
}

tests/benchmark/PrefixBench.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/*
4+
* SPDX-License-Identifier: MIT
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
* SPDX-FileContributor: Alexandre Gomes Gaigalas <alganet@gmail.com>
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Respect\Validation\Benchmarks;
12+
13+
use PhpBench\Attributes as Bench;
14+
use Respect\Validation\Transformers\Prefix;
15+
use Respect\Validation\Transformers\ValidatorSpec;
16+
17+
final class PrefixBench
18+
{
19+
/** @param array{0: Prefix, 1: ValidatorSpec} $params */
20+
#[Bench\ParamProviders(['provideTransformerSpec'])]
21+
#[Bench\Iterations(10)]
22+
#[Bench\RetryThreshold(5)]
23+
#[Bench\Revs(100)]
24+
#[Bench\Warmup(1)]
25+
#[Bench\Subject]
26+
public function prefixTransformer(array $params): void
27+
{
28+
$params[0]->transform($params[1]);
29+
}
30+
31+
/** @return array<array{0: Prefix, 1: ValidatorSpec}> */
32+
public static function provideTransformerSpec(): array
33+
{
34+
return [
35+
[new Prefix(), new ValidatorSpec('keyName', ['value', 'other'])],
36+
[new Prefix(), new ValidatorSpec('propertyTitle', ['value', 'other'])],
37+
[new Prefix(), new ValidatorSpec('notSomething', ['value'])],
38+
[new Prefix(), new ValidatorSpec('not')],
39+
[new Prefix(), new ValidatorSpec('arrayVal')],
40+
];
41+
}
42+
}

0 commit comments

Comments
 (0)