When processing API input, checking whether a value is valid is only part of the job.
Real input often looks more like this:
{
"title": " My first article ",
"visibility": "public",
"tags": [" PHP ", "API", "php"],
"publishedAt": "2026-08-20 10:30:00"
}
Before this data is useful to an application, several things usually need to happen:
- validate it;
- normalize whitespace;
- convert values to application-friendly types;
- normalize arrays recursively;
- remove duplicates;
- validate relationships between fields;
- convert dates and enums;
- expose predictable PHP values to the rest of the application.
That is the problem Small Forms is designed to solve.
Version 2.2.0 expands that idea with new validators, modifiers, collection helpers and form-aware validation rules.
Install it with Composer:
composer require small/forms
Small Forms is framework-agnostic. It is not trying to replace a complete web framework or a full form system.
It focuses on one specific problem:
Take external input, describe its expected structure, validate it, normalize it and expose clean PHP values with as little plumbing as possible.
Validation is excellent. Normalization is a different problem.
Symfony Validator already provides a very large collection of constraints and is a mature solution for validating values and object graphs.
The interesting difference with Small Forms is what happens around validation.
Validation answers:
Is this input acceptable?
But API input frequently needs another step:
What should this value look like when the application uses it?
For example, a valid string may still need trimming, lowercasing, enum conversion, JSON decoding or mapping over a collection.
Small Forms makes that normalization step part of the input definition itself.
The pipeline is intentionally simple:
input
↓
field definition
↓
validation rules
↓
modifiers
↓
clean PHP value
Validation and normalization belong next to each other
Consider a simple article payload.
use Small\Collection\Collection\StringCollection;
use Small\Forms\Form\Field\Type\ArrayType;
use Small\Forms\Form\Field\Type\StringType;
use Small\Forms\Form\FormBuilder;
use Small\Forms\Modifier\MapModifier;
use Small\Forms\Modifier\NormalizeWhitespaceModifier;
use Small\Forms\Modifier\StringToEnumModifier;
use Small\Forms\Modifier\ToLowerModifier;
use Small\Forms\Modifier\TrimModifier;
use Small\Forms\Modifier\UniqueArrayModifier;
use Small\Forms\ValidationRule\ValidateEach;
use Small\Forms\ValidationRule\ValidateEnum;
use Small\Forms\ValidationRule\ValidateNotEmpty;
use Small\Forms\ValidationRule\ValidateString;
enum Visibility: string
{
case Public = 'public';
case Private = 'private';
}
$form = FormBuilder::createInlineForm()
->addField(
'title',
new StringType(),
[
new ValidateNotEmpty(),
],
[
new TrimModifier(),
new NormalizeWhitespaceModifier(),
],
)
->addField(
'visibility',
new StringType(),
[
new ValidateEnum(Visibility::class),
],
[
new StringToEnumModifier(Visibility::class),
],
)
->addField(
'tags',
new ArrayType(new StringType()),
[
new ValidateEach(new ValidateString()),
],
[
new MapModifier(new TrimModifier()),
new MapModifier(new ToLowerModifier()),
new UniqueArrayModifier(),
],
);
Now fill it directly from JSON:
$form->fillFromJson(<<<JSON
{
"title": " My first article ",
"visibility": "public",
"tags": [" PHP ", "API", "php"]
}
JSON);
$messages = new StringCollection();
$form->validate($messages);
And retrieve normalized values:
$title = $form->getFieldValue('title');
// "My first article"
$visibility = $form->getFieldValue('visibility');
// Visibility::Public
$tags = $form->getFieldValue('tags');
// ["php", "api"]
The interesting part is not that trimming a string or converting an enum is difficult.
The advantage is that normalization becomes reusable and declarative.
There is no cleanup block hidden in a controller:
$title = trim($payload['title']);
$title = preg_replace(...);
$tags = array_map(...);
$tags = array_unique(...);
The normalization rules live with the input contract.
MapModifier: one abstraction for collection normalization
One of the most useful additions is MapModifier.
It applies any modifier to every value in an array or collection:
new MapModifier(
new TrimModifier(),
);
or:
new MapModifier(
new ToLowerModifier(),
);
That means a field can express a complete normalization pipeline:
[
new MapModifier(new TrimModifier()),
new MapModifier(new ToLowerModifier()),
new UniqueArrayModifier(),
]
The same compositional idea exists on the validation side:
new ValidateEach(
new ValidateString(),
);
The mental model stays consistent:
-
ValidateEachvalidates every item; -
MapModifiertransforms every item.
This is particularly useful for API payloads because validation and normalization are often needed on the same collection.
Cross-field validation without callback boilerplate
Input fields rarely exist independently.
Typical rules include:
confirmPassword must equal password
vatNumber is required if accountType is business
phoneCountry is required when phone is provided
fallbackContact is required when email is empty
Small Forms 2.2.0 provides explicit form-aware validators for these cases.
$form = FormBuilder::createInlineForm()
->addField(
'password',
new StringType(),
)
->addField(
'confirmPassword',
new StringType(),
[
new ValidateSameAsField('password'),
],
)
->addField(
'accountType',
new StringType(),
)
->addField(
'vatNumber',
new StringType(),
[
new ValidateRequiredIf(
'accountType',
'business',
),
],
)
->addField(
'phone',
new StringType(),
)
->addField(
'phoneCountry',
new StringType(),
[
new ValidateRequiredWith('phone'),
],
);
The validators automatically receive the current form context and can read sibling values.
Symfony can perform cross-property validation, but common conditional rules are often expressed through generic mechanisms such as callbacks or expressions.
For common API rules, this:
new ValidateRequiredIf(
'accountType',
'business',
)
is easier to discover and read than repeating custom conditional logic.
Generic callbacks are still useful when a rule is genuinely domain-specific.
But common relationships deserve common names.
Dates: validate one representation, expose another
Dates are another good example.
An API may receive:
2026-08-20 10:30:00
while the application would rather work with:
DateTimeImmutable
Small Forms can describe both requirements on the same field:
$form->addField(
'publishedAt',
new StringType(),
[
new ValidateDateAfter(
'2026-01-01 00:00:00',
),
],
[
new StringToDateTimeImmutableModifier(),
],
);
Then:
$publishedAt = $form->getFieldValue('publishedAt');
assert($publishedAt instanceof DateTimeImmutable);
The field definition answers two different questions:
What is acceptable input?
and:
What value should my application receive?
without scattering that logic across unrelated application code.
Array structures are first-class input
Small Forms 2.2.0 also adds more tools for structured input:
new ValidateArrayKeys(...);
new ValidateArrayShape(...);
new ValidateArrayItem(...);
new ValidateEach(...);
new ValidateAll(...);
new ValidateNoneOf(...);
The advantage becomes clear when structure validation is combined with Small Forms types and modifiers.
For example:
$form->addField(
'tags',
new ArrayType(
new StringType(),
),
[
new ValidateEach(
new ValidateString(),
),
],
[
new MapModifier(
new TrimModifier(),
),
new MapModifier(
new ToLowerModifier(),
),
new UniqueArrayModifier(),
],
);
The same field describes:
container type
+ item validation
+ item normalization
+ collection normalization
That composition is the main benefit.
Modifiers are intentionally small
Small Forms 2.2.0 includes modifiers for common input cleanup and conversion, including:
ToIntModifierToFloatModifierToStringModifierStringToBooleanModifierStringToEnumModifierNormalizeWhitespaceModifierNullIfBlankModifierEmptyStringToNullModifierJsonDecodeModifierJsonEncodeModifierCsvToArrayModifierReplaceModifierRegexReplaceModifierStripTagsModifierUniqueArrayModifierFilterEmptyArrayModifierSortArrayModifierLowercaseArrayModifierMapModifierSplitModifierJoinModifier
Each modifier does one small job.
Their value comes from composition.
[
new TrimModifier(),
new NormalizeWhitespaceModifier(),
new ToLowerModifier(),
]
is easier to reuse, test and reason about than one large generic sanitizeInput() helper.
And because modifiers implement the same interface, application-specific modifiers can participate in exactly the same pipeline.
The same principle applies to validation
The release also adds validators for common API input such as:
- enums;
- date boundaries;
- URLs;
- UUIDs;
- ULIDs;
- IP addresses;
- CIDR ranges;
- hostnames;
- array keys and shapes;
- filesystem paths;
- cross-field relationships.
Some of these have direct equivalents in Symfony Validator.
That is not the differentiator.
Small Forms is useful because those validators live inside the same compact input abstraction as:
types
modifiers
nested forms
arrays
JSON input
URL-encoded input
object input
hydration
The package is focused on the complete input pipeline rather than validation alone.
Why not just write helper functions?
You can.
For a small endpoint, that may be the right solution.
But helpers tend to grow:
trimInput();
normalizeBoolean();
validateEmail();
normalizeTags();
validateTags();
validateDates();
checkRequiredFields();
checkConditionalFields();
Then each endpoint has to decide which helper runs first.
Eventually, execution order becomes part of the input contract, but that contract is scattered through procedural code.
A declarative field definition makes the sequence visible:
$form->addField(
'tags',
new ArrayType(new StringType()),
validationRules: [
new ValidateEach(
new ValidateString(),
),
],
modifiers: [
new MapModifier(
new TrimModifier(),
),
new MapModifier(
new ToLowerModifier(),
),
new UniqueArrayModifier(),
],
);
You can read the input behavior directly from the field definition.
Small Forms is not trying to replace Symfony
Symfony Validator is mature, powerful and much broader in scope.
Small Forms targets a narrower use case:
Backend input where validation and normalization should be defined together without introducing a large amount of plumbing.
That makes it particularly useful for:
REST endpoints
JSON APIs
webhook payloads
CLI input
message consumers
integration layers
application service input
The goal is not “less Symfony”.
The goal is:
less glue code between external input and clean application values.
What changed in 2.2.0
The direction of Small Forms 2.2.0 can be summarized in one sentence:
Make common input transformations and field relationships composable enough that application code no longer needs to care about them.
Validation tells you:
Can I accept this input?
Modifiers answer:
What should this value look like when the application uses it?
Small Forms treats both as first-class parts of the same input definition.
If your application spends a lot of code going from:
$requestPayload
to:
$cleanApplicationValues
that is exactly the gap Small Forms is trying to fill.
composer require small/forms
repository : https://packagist.org/packages/small/forms
Small Forms is open source and released under the MIT license.
Top comments (0)