Skip to content

Commit e0088f0

Browse files
authored
Merge pull request #36 from studio-design/refactor/validator-cache-optimization
Refactoring: OpenApiResponseValidator のキャッシュ最適化
2 parents 6a429bc + 8fa937a commit e0088f0

5 files changed

Lines changed: 48 additions & 13 deletions

src/Laravel/ValidatesOpenApiSchema.php

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@
1919
trait ValidatesOpenApiSchema
2020
{
2121
use OpenApiSpecResolver;
22+
private static ?OpenApiResponseValidator $cachedValidator = null;
23+
private static ?int $cachedMaxErrors = null;
24+
25+
public static function resetValidatorCache(): void
26+
{
27+
self::$cachedValidator = null;
28+
self::$cachedMaxErrors = null;
29+
}
2230

2331
protected function openApiSpec(): string
2432
{
@@ -61,10 +69,7 @@ protected function assertResponseMatchesOpenApiSchema(
6169

6270
$contentType = $response->headers->get('Content-Type', '');
6371

64-
$maxErrors = config('openapi-contract-testing.max_errors', 20);
65-
$validator = new OpenApiResponseValidator(
66-
maxErrors: is_numeric($maxErrors) ? (int) $maxErrors : 20,
67-
);
72+
$validator = self::getOrCreateValidator();
6873
$result = $validator->validate(
6974
$specName,
7075
$resolvedMethod,
@@ -92,6 +97,19 @@ protected function assertResponseMatchesOpenApiSchema(
9297
);
9398
}
9499

100+
private static function getOrCreateValidator(): OpenApiResponseValidator
101+
{
102+
$maxErrors = config('openapi-contract-testing.max_errors', 20);
103+
$resolvedMaxErrors = is_numeric($maxErrors) ? (int) $maxErrors : 20;
104+
105+
if (self::$cachedValidator === null || self::$cachedMaxErrors !== $resolvedMaxErrors) {
106+
self::$cachedValidator = new OpenApiResponseValidator(maxErrors: $resolvedMaxErrors);
107+
self::$cachedMaxErrors = $resolvedMaxErrors;
108+
}
109+
110+
return self::$cachedValidator;
111+
}
112+
95113
/** @return null|array<string, mixed> */
96114
private function extractJsonBody(TestResponse $response, string $content, string $contentType): ?array
97115
{

src/OpenApiResponseValidator.php

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323

2424
final class OpenApiResponseValidator
2525
{
26+
/** @var array<string, OpenApiPathMatcher> */
27+
private array $pathMatchers = [];
28+
private Validator $opisValidator;
29+
private ErrorFormatter $errorFormatter;
30+
2631
public function __construct(
2732
private readonly int $maxErrors = 20,
2833
) {
@@ -31,6 +36,13 @@ public function __construct(
3136
sprintf('maxErrors must be 0 (unlimited) or a positive integer, got %d.', $this->maxErrors),
3237
);
3338
}
39+
40+
$resolvedMaxErrors = $this->maxErrors === 0 ? PHP_INT_MAX : $this->maxErrors;
41+
$this->opisValidator = new Validator(
42+
max_errors: $resolvedMaxErrors,
43+
stop_at_first_error: $resolvedMaxErrors === 1,
44+
);
45+
$this->errorFormatter = new ErrorFormatter();
3446
}
3547

3648
public function validate(
@@ -47,7 +59,7 @@ public function validate(
4759

4860
/** @var string[] $specPaths */
4961
$specPaths = array_keys($spec['paths'] ?? []);
50-
$matcher = new OpenApiPathMatcher($specPaths, OpenApiSpecLoader::getStripPrefixes());
62+
$matcher = $this->getPathMatcher($specName, $specPaths);
5163
$matchedPath = $matcher->match($requestPath);
5264

5365
if ($matchedPath === null) {
@@ -135,19 +147,13 @@ public function validate(
135147
$schemaObject = self::toObject($jsonSchema);
136148
$dataObject = self::toObject($responseBody);
137149

138-
$resolvedMaxErrors = $this->maxErrors === 0 ? PHP_INT_MAX : $this->maxErrors;
139-
$validator = new Validator(
140-
max_errors: $resolvedMaxErrors,
141-
stop_at_first_error: $resolvedMaxErrors === 1,
142-
);
143-
$result = $validator->validate($dataObject, $schemaObject);
150+
$result = $this->opisValidator->validate($dataObject, $schemaObject);
144151

145152
if ($result->isValid()) {
146153
return OpenApiValidationResult::success($matchedPath);
147154
}
148155

149-
$formatter = new ErrorFormatter();
150-
$formattedErrors = $formatter->format($result->error());
156+
$formattedErrors = $this->errorFormatter->format($result->error());
151157

152158
$errors = [];
153159
foreach ($formattedErrors as $path => $messages) {
@@ -187,6 +193,14 @@ private static function toObject(mixed $value): mixed
187193
return $object;
188194
}
189195

196+
/**
197+
* @param string[] $specPaths
198+
*/
199+
private function getPathMatcher(string $specName, array $specPaths): OpenApiPathMatcher
200+
{
201+
return $this->pathMatchers[$specName] ??= new OpenApiPathMatcher($specPaths, OpenApiSpecLoader::getStripPrefixes());
202+
}
203+
190204
/**
191205
* Find the first JSON-compatible content type from the response spec.
192206
*

tests/Unit/ValidatesOpenApiSchemaAttributeTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ protected function setUp(): void
3333

3434
protected function tearDown(): void
3535
{
36+
self::resetValidatorCache();
3637
OpenApiSpecLoader::reset();
3738
OpenApiCoverageTracker::reset();
3839
parent::tearDown();

tests/Unit/ValidatesOpenApiSchemaDefaultSpecTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ protected function setUp(): void
4141

4242
protected function tearDown(): void
4343
{
44+
self::resetValidatorCache();
4445
unset($GLOBALS['__openapi_testing_config']);
4546
OpenApiSpecLoader::reset();
4647
OpenApiCoverageTracker::reset();

tests/Unit/ValidatesOpenApiSchemaTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ protected function setUp(): void
3232

3333
protected function tearDown(): void
3434
{
35+
self::resetValidatorCache();
3536
OpenApiSpecLoader::reset();
3637
OpenApiCoverageTracker::reset();
3738
parent::tearDown();

0 commit comments

Comments
 (0)