Skip to content

Commit fe5a989

Browse files
committed
docs: add guide for extending core validators
This section explains how to create domain-specific validators by subclassing core library components. Includes a detailed example of a DomainValidator to demonstrate the pattern.
1 parent 7ab5ee3 commit fe5a989

1 file changed

Lines changed: 88 additions & 0 deletions

File tree

docs/guides/custom-validation.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,94 @@ $conditionalValidator = Validator::isString()->satisfies(
326326
);
327327
```
328328

329+
## Extending Core Validators
330+
331+
Sometimes a project needs a richer, domain-specific validator (e.g., domains, SKU formats, internal identifiers) with its own fluent helpers. You can create these by subclassing the appropriate Lemmon validator and preconfiguring the shared pipeline logic.
332+
333+
### Example: Domain Validator With Convenience Methods
334+
335+
```php
336+
namespace App\Validation;
337+
338+
use App\Domain;
339+
use Lemmon\Validator\StringValidator;
340+
341+
final class DomainValidator extends StringValidator
342+
{
343+
public function __construct()
344+
{
345+
parent::__construct();
346+
347+
// Build on top of the standard string validator pipeline
348+
$this->pipe(
349+
'trim',
350+
'strtolower',
351+
fn($value) => str_starts_with($value, 'www.') ? substr($value, 4) : $value,
352+
fn($value) => rtrim($value, '.')
353+
)
354+
->nullifyEmpty()
355+
->satisfies(
356+
fn($value) => substr_count($value, '.') > 0
357+
&& filter_var($value, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME),
358+
'HOSTNAME_INVALID'
359+
);
360+
}
361+
362+
public function ensureUnique(): self
363+
{
364+
return $this->satisfies(
365+
fn($value) => !Domain::fromDomain($value, silent: true),
366+
'DOMAIN_DUPLICATE'
367+
);
368+
}
369+
370+
public function mustBeRoot(): self
371+
{
372+
return $this->satisfies(
373+
fn($value) => substr_count($value, '.') === 1,
374+
'DOMAIN_NOT_ROOT'
375+
);
376+
}
377+
378+
public function mustBeSubdomainOf(string $root): self
379+
{
380+
$root = ltrim($root, '.');
381+
382+
return $this->satisfies(
383+
fn($value) => substr_count($value, '.') > 1
384+
&& str_ends_with($value, '.' . $root),
385+
'DOMAIN_NOT_ALLOWED'
386+
);
387+
}
388+
}
389+
390+
final class ValidationRules
391+
{
392+
public static function isDomain(): DomainValidator
393+
{
394+
return new DomainValidator();
395+
}
396+
}
397+
```
398+
399+
Usage stays fully fluent:
400+
401+
```php
402+
$domain = ValidationRules::isDomain()
403+
->ensureUnique()
404+
->mustBeSubdomainOf('example.com')
405+
->validate('blog.example.com');
406+
```
407+
408+
This pattern scales to any custom validator:
409+
410+
- Subclass the Lemmon validator that matches your base type (`StringValidator`, `IntValidator`, etc.)
411+
- Configure the shared pipeline in `__construct` (coercion, transforms, generic `satisfies()` calls)
412+
- Add expressive helper methods that append more rules and return `$this`
413+
- Return the custom validator from a project-specific factory (e.g., `ValidationRules::isDomain()`)
414+
415+
Because these classes extend the core validators, they inherit null handling, error aggregation, coercion, and every other built-in capability, while letting you publish clean, reusable validators for your application.
416+
329417
## Testing Custom Validators
330418

331419
### Unit Testing Custom Validation Logic

0 commit comments

Comments
 (0)