Skip to content

Commit 5784096

Browse files
authored
Merge pull request #1 from gravity-zero/core_optimizations
feat: improve SimpleValidation, ConditionalValidationNotTriggered, CustomStrategy ~40%
2 parents 9d530b8 + 23316d5 commit 5784096

7 files changed

Lines changed: 219 additions & 150 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.0.3] - 2026-01-04
9+
### Performance
10+
- Lazy TranslationManager initialization - 45% performance improvement
11+
- TranslationManager now created only when validation fails or explicitly configured
12+
- Eliminates unnecessary I/O for successful validations (~90% of production cases)
13+
814
## [1.0.2] - 2026-01-03
915
### Added
1016
- New `url` validation rule with configurable schemes and TLD handling

benchmarks/bench_results.csv

Lines changed: 105 additions & 105 deletions
Large diffs are not rendered by default.

docs/BENCHMARK.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,31 @@ make benchmark-stats # With P50/P95/P99 percentiles
1111
### Results (PHP 8.5.1)
1212

1313
**Core Operations** (P99):
14-
- Simple validation: **49.6μs** (99% < 50μs)
15-
- Complex nested: **71.8μs** (99% < 72μs)
16-
- Custom strategy: **53.6μs** (99% < 54μs)
14+
- Simple validation: **22.3μs** (99% < 23μs)
15+
- Complex nested: **41.7μs** (99% < 42μs)
16+
- Custom strategy: **21.4μs** (99% < 22μs)
1717

1818
**Batch Processing:**
19-
- Batch mode (100 fields): **1.5ms**
20-
- Fail-fast mode (100 fields): **0.68ms****2.1x faster**
19+
- Batch mode (100 fields): **1.42ms**
20+
- Fail-fast mode (100 fields): **0.65ms****2.2x faster**
2121

2222
**Conditional Validations** (P99):
23-
- Triggered: **50.4μs**
24-
- Not triggered: **42.2μs** ← Faster (validation skipped)
25-
- Failed + errors: **62.7μs** ← Most expensive (error rendering)
26-
- Complex AND/OR: **~44μs**
23+
- Triggered: **24.2μs**
24+
- Not triggered: **16.3μs** ← Faster (validation skipped)
25+
- Failed + errors: **61μs** ← Most expensive (error rendering)
26+
- Complex AND/OR: **~17μs**
2727

2828
**Translation Overhead:**
29-
- With translation: **50.4μs**
30-
- Without translation: **48.5μs**
31-
- **Overhead: ~4%** (negligible)
29+
- With translation: **46μs**
30+
- Without translation: **47.5μs**
31+
- **Overhead: ~3%** (negligible)
3232

3333
**Memory:** ~4.9MB (stable, no leaks)
3434

3535
**Key Insights:**
36-
-**99% of validations complete in <72μs** (sub-millisecond)
36+
-**99% of validations complete in <42μs** (sub-millisecond)
3737
-**Very stable** - P99 within 5% of mean (low variance)
38-
-**Fail-fast mode 2x faster** when you need speed
38+
-**Fail-fast mode 2.2x faster** when you need speed
3939
-**Conditional skip is fast** - unused validations add minimal overhead
4040

4141
*Benchmarks: [PHPBench 1.4.3](https://github.com/phpbench/phpbench) • PHP 8.5.1 • No opcache/xdebug*

src/DataVerify.php

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,12 @@ class DataVerify
6161
private ValidationContext $context;
6262
private FieldCollection $fields;
6363
private ErrorCollection $errors;
64-
private TranslationManager $translationManager;
64+
private ?TranslationManager $translationManager = null;
6565
private bool $hasVerified = false;
6666

6767
// Engines
6868
private ValidationOrchestrator $orchestrator;
6969
private ConditionalEngine $conditionalEngine;
70-
private ErrorManager $errorManager;
7170
private DataTraverser $dataTraverser;
7271
private LazyValidationRegistry $lazyRegistry;
7372

@@ -77,7 +76,6 @@ public function __construct(array|object $data)
7776
$this->context = new ValidationContext();
7877
$this->fields = new FieldCollection();
7978
$this->errors = new ErrorCollection();
80-
$this->translationManager = new TranslationManager();
8179
$this->lazyRegistry = LazyValidationRegistry::instance();
8280

8381
$this->initializeEngines();
@@ -87,12 +85,10 @@ private function initializeEngines(): void
8785
{
8886
$this->dataTraverser = new DataTraverser($this->data);
8987
$this->conditionalEngine = new ConditionalEngine($this->dataTraverser);
90-
$this->errorManager = new ErrorManager($this->errors, $this->translationManager, $this->lazyRegistry);
9188

9289
$this->orchestrator = new ValidationOrchestrator(
9390
$this->fields,
9491
$this->errors,
95-
$this->errorManager,
9692
$this->dataTraverser,
9793
$this->conditionalEngine,
9894
new ValidationRegistry(),
@@ -261,27 +257,42 @@ public function alias(string $name): self
261257
return $this;
262258
}
263259

260+
/**
261+
* Get or create translation manager lazily
262+
*/
263+
private function getTranslationManager(): TranslationManager
264+
{
265+
if ($this->translationManager === null) {
266+
$this->translationManager = new TranslationManager();
267+
}
268+
269+
$errorManager = new ErrorManager($this->errors, $this->lazyRegistry, $this->translationManager);
270+
$this->orchestrator->setErrorManager($errorManager);
271+
272+
return $this->translationManager;
273+
}
274+
264275
public function setTranslator(TranslatorInterface $translator): self
265276
{
266-
$this->translationManager->setTranslator($translator);
277+
$this->getTranslationManager()->setTranslator($translator);
267278
return $this;
268279
}
269280

270281
public function setLocale(string $locale): self
271282
{
272-
$this->translationManager->setLocale($locale);
283+
$this->getTranslationManager()->setLocale($locale);
273284
return $this;
274285
}
275286

276287
public function addTranslations(array $translations, string $locale = 'en'): self
277288
{
278-
$this->translationManager->addTranslations($translations, $locale);
289+
$this->getTranslationManager()->addTranslations($translations, $locale);
279290
return $this;
280291
}
281292

282293
public function loadLocale(string $locale, ?string $filePath = null): self
283294
{
284-
$this->translationManager->loadLocale($locale, $filePath);
295+
$this->getTranslationManager()->loadLocale($locale, $filePath);
285296
return $this;
286297
}
287298

@@ -423,4 +434,4 @@ public static function enableIdeHelper(?string $outputPath = null): void
423434
{
424435
\Gravity\Documentation\IdeHelperManager::instance()->enable($outputPath);
425436
}
426-
}
437+
}

src/Engine/ErrorManager.php

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
<?php
2-
32
namespace Gravity\Engine;
43

54
use Gravity\Collections\ErrorCollection;
@@ -12,14 +11,26 @@
1211
* ErrorManager
1312
*
1413
* Responsible for error creation, message translation, and parameter extraction.
14+
* Optimized with metadata caching to avoid repeated lookups.
1515
*/
1616
class ErrorManager
1717
{
18+
private ?TranslationManager $translationManager = null;
19+
20+
/**
21+
* Cache for resolved metadata (validation name → metadata)
22+
* Avoids repeated registry lookups for the same validation
23+
* @var array<string, ValidationMetadata|null>
24+
*/
25+
private array $metadataCache = [];
26+
1827
public function __construct(
1928
private ErrorCollection $errors,
20-
private TranslationManager $translationManager,
21-
private LazyValidationRegistry $registry
22-
) {}
29+
private LazyValidationRegistry $registry,
30+
?TranslationManager $translationManager = null
31+
) {
32+
$this->translationManager = $translationManager;
33+
}
2334

2435
/**
2536
* Add validation error with translated message
@@ -36,7 +47,7 @@ public function addError(
3647

3748
if (!$errorMessage) {
3849
$params = $this->buildValidationParams($testName, $args);
39-
$errorMessage = $this->translationManager->getValidationMessage(
50+
$errorMessage = $this->getTranslationManager()->getValidationMessage(
4051
$testName,
4152
$alias,
4253
$value,
@@ -51,32 +62,48 @@ public function addError(
5162

5263
/**
5364
* Build parameters from args for error messages
65+
* Optimized with metadata caching
5466
*/
5567
private function buildValidationParams(string $testName, array $args): array
5668
{
69+
if (empty($args)) {
70+
return [];
71+
}
72+
5773
$metadata = $this->resolveMetadata($testName);
58-
if (!$metadata) {
74+
if (!$metadata || empty($metadata->parameters)) {
5975
return [];
6076
}
6177

6278
return $this->mapArgsToParams($metadata, $args);
6379
}
6480

65-
81+
/**
82+
* Resolve metadata with caching
83+
*/
6684
private function resolveMetadata(string $testName): ?ValidationMetadata
6785
{
68-
return $this->registry->get($testName)
86+
if (array_key_exists($testName, $this->metadataCache)) {
87+
return $this->metadataCache[$testName];
88+
}
89+
90+
$metadata = $this->registry->get($testName)
6991
?? GlobalStrategyRegistry::instance()->getAllMetadata()[$testName]
7092
?? null;
93+
94+
$this->metadataCache[$testName] = $metadata;
95+
96+
return $metadata;
7197
}
7298

99+
/**
100+
* Map args to parameter names
101+
* ✅ Optimisation : early return si pas de parameters
102+
*/
73103
private function mapArgsToParams(ValidationMetadata $metadata, array $args): array
74104
{
75-
if (empty($metadata->parameters)) {
76-
return [];
77-
}
78-
79105
$params = [];
106+
80107
foreach ($metadata->parameters as $i => $p) {
81108
$name = $p['name'];
82109
$value = $args[$i] ?? ($p['default'] ?? null);
@@ -93,9 +120,21 @@ private function mapArgsToParams(ValidationMetadata $metadata, array $args): arr
93120

94121
/**
95122
* Get translation manager for external configuration
123+
* Lazy-loads if not already set
96124
*/
97125
public function getTranslationManager(): TranslationManager
98126
{
127+
if ($this->translationManager === null) {
128+
$this->translationManager = new TranslationManager();
129+
}
99130
return $this->translationManager;
100131
}
132+
133+
/**
134+
* Set translation manager (for user customization)
135+
*/
136+
public function setTranslationManager(TranslationManager $translationManager): void
137+
{
138+
$this->translationManager = $translationManager;
139+
}
101140
}

src/Engine/ValidationOrchestrator.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,37 @@
2121
*/
2222
class ValidationOrchestrator
2323
{
24+
private ?ErrorManager $errorManager = null;
25+
2426
public function __construct(
2527
private FieldCollection $fields,
2628
private ErrorCollection $errors,
27-
private ErrorManager $errorManager,
2829
private DataTraverser $dataTraverser,
2930
private ConditionalEngine $conditionalEngine,
3031
private ValidationRegistry $registry,
3132
private LazyValidationRegistry $lazyRegistry
3233
) {
3334
// No upfront initialization needed - lazy loading handles it
3435
}
36+
37+
/**
38+
* Get or create ErrorManager lazily (only when validation fails)
39+
*/
40+
private function getErrorManager(): ErrorManager
41+
{
42+
if ($this->errorManager === null) {
43+
$this->errorManager = new ErrorManager($this->errors, $this->lazyRegistry);
44+
}
45+
return $this->errorManager;
46+
}
47+
48+
/**
49+
* Set ErrorManager (for external configuration like translations)
50+
*/
51+
public function setErrorManager(ErrorManager $errorManager): void
52+
{
53+
$this->errorManager = $errorManager;
54+
}
3555

3656
/**
3757
* Execute all validations
@@ -135,7 +155,7 @@ private function executeValidation(
135155
if ($test === 'required' || !$this->dataTraverser->isValueEmpty($value)) {
136156
$result = $this->runValidationTest($test, $value, $args);
137157
if (!$result) {
138-
$this->errorManager->addError($handler, $test, $value, $path, $args);
158+
$this->getErrorManager()->addError($handler, $test, $value, $path, $args);
139159
}
140160
}
141161
}

src/Registry/LazyValidationRegistry.php

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,17 +124,12 @@ private function discoverValidationClass(string $name): ?string
124124
return $this->discoveredClasses[$name];
125125
}
126126

127-
// Convert name to class name
128-
// 'minLength' → 'MinLength'
129-
// 'email' → 'Email'
130127
$className = ucfirst($name) . 'Validation';
131128

132-
// Try each directory
133129
foreach (self::VALIDATION_DIRS as $dir) {
134130
$fullClassName = self::VALIDATION_NAMESPACE . $dir . '\\' . $className;
135131

136132
if (class_exists($fullClassName)) {
137-
// Verify it implements the interface
138133
if (is_subclass_of($fullClassName, ValidationStrategyInterface::class)) {
139134
$this->discoveredClasses[$name] = $fullClassName;
140135
return $fullClassName;
@@ -180,14 +175,12 @@ public function getAvailableValidations(): array
180175
continue;
181176
}
182177

183-
// Instantiate to get the name
184178
try {
185179
$instance = new $fullClassName();
186180
if ($instance instanceof ValidationStrategyInterface) {
187181
$validations[] = $instance->getName();
188182
}
189183
} catch (\Throwable $e) {
190-
// Skip invalid classes
191184
continue;
192185
}
193186
}

0 commit comments

Comments
 (0)