laminas/laminas-inputfilter
Laminas InputFilter provides a robust, extensible system for filtering and validating input data in PHP applications, with composable filter and validator chains, nested input handling, and error reporting for reliable form and API data processing.
Install the package and let Composer pull in its siblings (laminas-filter, laminas-validator, laminas-servicemanager):
composer require laminas/laminas-inputfilter
The mental model is small and consistent: an InputFilter is a named collection of Input objects. Each Input owns a filter chain (normalization) and a validator chain (assertions). Filters run first, then validators, and only after isValid() do you read getValues().
First use case — validating a registration or API payload:
use Laminas\Filter\StringTrim;
use Laminas\InputFilter\InputFilter;
use Laminas\Validator\EmailAddress;
use Laminas\Validator\StringLength;
$filter = new InputFilter();
$filter->add([
'name' => 'email',
'required' => true,
'filters' => [['name' => StringTrim::class]],
'validators' => [['name' => EmailAddress::class]],
]);
$filter->add([
'name' => 'password',
'required' => true,
'validators' => [
['name' => StringLength::class, 'options' => ['min' => 12]],
],
]);
$filter->setData($requestData);
if (! $filter->isValid()) {
// ['email' => ['emailAddressInvalid' => '...'], ...]
$errors = $filter->getMessages();
} else {
$clean = $filter->getValues(); // filtered, unknown keys stripped
}
Where to look first: src/InputFilter.php, src/Input.php, src/Factory.php, src/CollectionInputFilter.php, and src/FileInput.php. Then read docs/book/ for the spec-array format; the tests are the best reference for edge-case behavior around emptiness and chains.
Prefer spec arrays over fluent add() calls. Specs are portable, diffable, and can live in configuration or be reused between HTTP, CLI, and queue workers. Factory::createInputFilter() builds nested filters, collections, and file inputs recursively:
$factory = new \Laminas\InputFilter\Factory();
$filter = $factory->createInputFilter([
'email' => [
'required' => true,
'filters' => [['name' => \Laminas\Filter\StringTrim::class]],
'validators' => [['name' => \Laminas\Validator\EmailAddress::class]],
],
'profile' => [
'type' => \Laminas\InputFilter\InputFilter::class,
'input_filter' => [
'display_name' => ['required' => true],
'bio' => ['required' => false],
],
],
]);
Wrap reusable filters in classes. For anything used in more than one endpoint, subclass InputFilter or implement InputFilterAwareInterface; for laminas-form/entity wiring, implement InputFilterProviderInterface::getInputFilterSpecification(). This keeps controller code to setData() / isValid() / getValues().
Resolve from the container where the framework integration exists. In laminas-mvc, spec arrays under the input_filter_specs config key are retrievable via the InputFilterManager. In Mezzio/standalone, pull InputFilterPluginManager from the container or invoke Factory directly with specs kept in your own config. Register custom filters and validators through the filters/validators plugin configuration and reference them by FQCN to avoid alias drift.
Use validation groups for partial updates. PATCH endpoints should not require the full payload:
$filter->setValidationGroup(['email']); // top-level only
$filter->setValidationGroup(['profile' => ['bio']]); // nested group
Express cross-field rules with Callback, not filters. Callback validators receive the whole raw context as a second argument, which is the right place for "state is required when country is US" style logic.
Use CollectionInputFilter for repeated rows. Attach a row-level input filter, feed it the array of rows, and set an explicit count when cardinality is contractual (e.g. exactly three addresses); otherwise let it derive from the data. Validate file uploads with FileInput plus Laminas\Validator\File\UploadFile, MimeType, Extension, and Size, then move the file yourself from getValue()['tmp_name'].
Treat the input filter as the normalization boundary. Do trimming, ToInt, boolean coercion, and empty-to-null conversion in filters so services and repositories only ever see getValues(). One filter per endpoint/command, reused across HTTP and CLI, is usually the sweet spot.
StringTrim becomes '' and can then fail required — even though the raw value looked present. Order filters deliberately.required, allow_empty, and continue_if_empty are the number-one source of confusion. Required means "must be present"; allow_empty => false means "if present, must not be empty"; continue_if_empty => true means "still run validators on empty values." That last one is useful for optional fields with a format rule, but the validator will happily fail on '', so guard it with a Callback or accept the failure deliberately.'', null, and [] are treated as empty. JSON null and a missing key are not always semantically the same; decide per field and test it.getValues() is only meaningful after isValid(). It returns filtered values and silently strips unknown keys. Use getRawValues() for debugging, never for persistence.StringTrim, Int, etc.).break_chain_on_failure per validator and break_on_failure per input save cycles on large payloads, but they hide additional messages. Enable them only when you don't need a full error report.CollectionInputFilter cardinality is easy to get wrong. Set setCount() when the count is part of the contract; otherwise verify against your data shape rather than assuming it is inferred.FileInput is a validator, not an uploader. It validates $_FILES-shaped arrays and holds the metadata; you still need move_uploaded_file() and explicit UPLOAD_ERR_* handling.setData() requires an array or Traversable. Decode PSR-7 JSON bodies first and reject malformed JSON before the filter sees it, otherwise you get an InvalidArgumentException instead of a validation error.getRawValues(), then getValues(), then getMessages(). If a value appears "missing," check whether a filter converted it to '' or whether required/allow_empty short-circuited the validators.OptionalInputFilter and newer filter class names (e.g. ToInt) are recent additions. Pin the package version and favor FQCNs over legacy aliases to keep upgrades boring.How can I help you explore Laravel packages today?