Skip to content
60 changes: 59 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ offering an easy-to-use and intuitive API to validate user input or other data i
## Table of Contents

- [Installation](#installation)
- [When to use it](#when-to-use-it)
- [Usage](#usage)
- [Constraints](#constraints)
- [Methods](#methods)
Expand All @@ -30,6 +31,7 @@ offering an easy-to-use and intuitive API to validate user input or other data i
- [toArray](#toarray)
- [addNamespace](#addnamespace)
- [setTranslator](#settranslator)
- [reset](#reset)
- [Custom Constraints](#custom-constraints)
- [Translations](#translations)

Expand All @@ -45,6 +47,13 @@ Install via [Composer](https://getcomposer.org/):
composer require programmatordev/fluent-validator
```

## When to use it

Use Fluent Validator when you want Symfony Validator constraints for raw values without setting up object metadata, attributes, forms, or a larger validation layer.
It is useful for small input checks, command arguments, request fragments, webhook payload values, configuration values, and library code.

This package does not replace Symfony Validator. It wraps Symfony Validator and keeps its constraints, violation objects, groups, translations, and custom constraint model.

## Usage

Simple usage example:
Expand All @@ -63,7 +72,33 @@ if ($errors->count() > 0) {
}
```

Use `assert` when invalid values should stop the current flow:

```php
use ProgrammatorDev\FluentValidator\Exception\ValidationFailedException;
use ProgrammatorDev\FluentValidator\Validator;

try {
Validator::notBlank()->email()->assert($email, 'email');
}
catch (ValidationFailedException $exception) {
$message = $exception->getMessage();
// "email: This value is not a valid email address."
}
```

Use `isValid` when you only need a boolean:

```php
use ProgrammatorDev\FluentValidator\Validator;

if (!Validator::url()->isValid($website)) {
// handle invalid URL
}
```

Constraint autocompletion is available in IDEs like PhpStorm.
The suggested methods are generated from the installed Symfony Validator constraints.
The method names match Symfony constraints but with a lowercase first letter:

- `NotBlank` => `notBlank`
Expand All @@ -77,6 +112,20 @@ For all available methods, check the [Methods](#methods) section.

There is also a section for [Custom Constraints](#custom-constraints) and [Translations](#translations).

### Groups

Validation groups work the same way as in Symfony Validator:

```php
use ProgrammatorDev\FluentValidator\Validator;

$validator = Validator::notBlank(groups: ['Default'])
->email(groups: ['registration']);

$validator->isValid('invalid-email', groups: ['Default']); // true
$validator->isValid('invalid-email', groups: ['registration']); // false
```

## Constraints

All available constraints can be found on the [Symfony Validator documentation](https://symfony.com/doc/current/validation.html#constraints).
Expand Down Expand Up @@ -209,6 +258,15 @@ Used to add a translator for validation error message translations.

Check the [Translations](#translations) section.

### `reset`

```php
reset(): void
```

Clears globally registered custom constraint namespaces and translator configuration.
Useful when changing global validator configuration in tests, workers, or other long-running PHP processes.

## Custom Constraints

If you need a custom constraint, follow the Symfony Validator documentation: [Creating Custom Constraints](https://symfony.com/doc/current/validation/custom_constraint.html).
Expand Down Expand Up @@ -291,4 +349,4 @@ Make sure to open a pull request or issue.
## License

This project is licensed under the MIT license.
Please see the [LICENSE](LICENSE) file distributed with this source code for further information regarding copyright and licensing.
Please see the [LICENSE](LICENSE) file distributed with this source code for further information regarding copyright and licensing.
10 changes: 5 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
],
"require": {
"php": ">=8.4",
"symfony/config": "^8.0",
"symfony/translation": "^8.0",
"symfony/validator": "^8.0"
"symfony/config": "^8.1",
"symfony/translation": "^8.1",
"symfony/validator": "^8.1"
},
"require-dev": {
"phpunit/phpunit": "^11.5",
"symfony/console": "^8.0",
"symfony/var-dumper": "^8.0",
"symfony/console": "^8.1",
"symfony/var-dumper": "^8.1",
"wyrihaximus/list-classes-in-directory": "^1.7"
},
"autoload": {
Expand Down
11 changes: 11 additions & 0 deletions src/ChainedValidatorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,17 @@ public function wordCount(
mixed $payload = null,
): ChainedValidatorInterface&Validator;

public function xml(
string $formatMessage = 'This value is not valid XML.',
string $schemaMessage = 'This value does not conform to the expected XSD schema.',
string $tooLargeMessage = 'This XML payload is too large ({{ size }} bytes): it exceeds the limit of {{ limit }} bytes.',
?string $schemaPath = null,
int $schemaFlags = 0,
int $maxSize = 5242880,
?array $groups = null,
mixed $payload = null,
): ChainedValidatorInterface&Validator;

public function yaml(
string $message = 'This value is not valid YAML.',
int $flags = 0,
Expand Down
13 changes: 13 additions & 0 deletions src/Exception/NoSuchTranslationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace ProgrammatorDev\FluentValidator\Exception;

class NoSuchTranslationException extends \RuntimeException
{
public function __construct(string $locale)
{
$message = sprintf('Translation for locale "%s" was not found.', $locale);

parent::__construct($message);
}
}
11 changes: 11 additions & 0 deletions src/StaticValidatorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,17 @@ public static function wordCount(
mixed $payload = null,
): ChainedValidatorInterface&Validator;

public static function xml(
string $formatMessage = 'This value is not valid XML.',
string $schemaMessage = 'This value does not conform to the expected XSD schema.',
string $tooLargeMessage = 'This XML payload is too large ({{ size }} bytes): it exceeds the limit of {{ limit }} bytes.',
?string $schemaPath = null,
int $schemaFlags = 0,
int $maxSize = 5242880,
?array $groups = null,
mixed $payload = null,
): ChainedValidatorInterface&Validator;

public static function yaml(
string $message = 'This value is not valid YAML.',
int $flags = 0,
Expand Down
7 changes: 6 additions & 1 deletion src/Translator/Translator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace ProgrammatorDev\FluentValidator\Translator;

use Composer\InstalledVersions;
use ProgrammatorDev\FluentValidator\Exception\NoSuchTranslationException;
use Symfony\Component\Translation\Loader\XliffFileLoader;
use Symfony\Contracts\Translation\TranslatorInterface;

Expand All @@ -16,6 +17,10 @@ public function __construct(private string $locale)
$packagePath = InstalledVersions::getInstallPath('symfony/validator');
$resourcePath = sprintf('%s/Resources/translations/validators.%s.xlf', $packagePath, $this->locale);

if (!is_file($resourcePath)) {
throw new NoSuchTranslationException($this->locale);
}

$this->translator = new \Symfony\Component\Translation\Translator($this->locale);
$this->translator->addLoader('xlf', new XliffFileLoader());
$this->translator->addResource('xlf', $resourcePath, $this->locale);
Expand All @@ -30,4 +35,4 @@ public function getLocale(): string
{
return $this->locale;
}
}
}
14 changes: 12 additions & 2 deletions src/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
class Validator
{
/** @var Constraint[] */
private array $constraints;
private array $constraints = [];

/** @var string[] */
private static array $namespaces = [];
Expand Down Expand Up @@ -99,11 +99,21 @@ private function addConstraint(Constraint $constraint): void

public static function addNamespace(string $namespace): void
{
if (in_array($namespace, self::$namespaces, true)) {
return;
}

self::$namespaces[] = $namespace;
}

public static function setTranslator(?TranslatorInterface $translator): void
{
self::$translator = $translator;
}
}

public static function reset(): void
{
self::$namespaces = [];
self::$translator = null;
}
}
25 changes: 9 additions & 16 deletions src/Writer/InterfaceWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ class InterfaceWriter
{
private \SplFileObject $file;

public function __construct(private readonly string $interfaceName)
public function __construct(
private readonly string $interfaceName,
?string $outputDirectory = null,
)
{
$filename = sprintf('src/%s.php', $this->interfaceName);
$outputDirectory ??= dirname(__DIR__);
$filename = sprintf('%s/%s.php', rtrim($outputDirectory, '/'), $this->interfaceName);

$this->file = new \SplFileObject($filename, 'w');
}

Expand Down Expand Up @@ -100,10 +105,6 @@ private function formatType(string $type): string

private function formatValue(mixed $value): string
{
if (is_string($value)) {
return sprintf("'%s'", $value);
}

if ($value === []) {
return '[]';
}
Expand All @@ -112,14 +113,6 @@ private function formatValue(mixed $value): string
return 'null';
}

if ($value === false) {
return 'false';
}

if ($value === true) {
return 'true';
}

return (string) $value;
return var_export($value, true);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

use PHPUnit\Framework\TestCase;

class AbstractTestCase extends TestCase {}
class AbstractTestCase extends TestCase {}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace ProgrammatorDev\FluentValidator\Test\Constraint;
namespace ProgrammatorDev\FluentValidator\Test\Fixtures\Constraint;

use Symfony\Component\Validator\Constraint;

Expand All @@ -24,4 +24,4 @@ public function __sleep(): array
{
return array_merge(parent::__sleep(), ['mode']);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace ProgrammatorDev\FluentValidator\Test\Constraint;
namespace ProgrammatorDev\FluentValidator\Test\Fixtures\Constraint;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
Expand Down Expand Up @@ -45,4 +45,4 @@ public function validate(mixed $value, Constraint $constraint): void
->setParameter('{{ string }}', $value)
->addViolation();
}
}
}
Loading
Loading