symfony/http-foundation · error · InvalidArgumentException
A Closure must be passed to
Error message
A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.
What it means
InputBag::filter() wraps PHP's filter_var(). When the filter is FILTER_CALLBACK, PHP requires the 'options' key to hold a Closure; the library detects a non-Closure (or missing) options value and throws this InvalidArgumentException up front with a clearer message than filter_var's silent failure. This is a pre-flight validation of the filter options array.
Solutions
- Wrap the callable in a Closure: ['options' => fn ($v) => strtoupper($v)].
- Use first-class callable syntax: ['options' => strtoupper(...)] which produces a Closure.
- If the callable is [$obj, 'method'], use Closure::fromCallable([$obj, 'method']) or $obj->method(...).
- Verify the filter constant actually needs FILTER_CALLBACK; a built-in filter like FILTER_VALIDATE_INT does not need options.
- Ensure the options array is not reused/overwritten so the 'options' key still holds the Closure at call time.
Example fix
// before
$bag->filter('name', \FILTER_CALLBACK, ['options' => 'trim']); // string callable: throws
// after
$bag->filter('name', \FILTER_CALLBACK, ['options' => fn ($v) => trim($v)]); Defensive patterns
Strategy: validation
Validate before calling
// before calling filter with FILTER_CALLBACK
$options = $options ?? [];
if (\FILTER_CALLBACK & $filter && !($options['options'] ?? null) instanceof \Closure) {
$options['options'] = $options['options'](...); // wrap callable in a Closure
} Type guard
function isClosure(mixed $c): bool {
return $c instanceof \Closure;
} Try / catch
try {
$value = $bag->filter($key, \FILTER_CALLBACK, ['options' => $callback]);
} catch (\InvalidArgumentException $e) {
$value = null; // or fall back to an unfiltered default
} Prevention
- Always use Closure syntax for FILTER_CALLBACK options: fn ($v) => ... or strtoupper(...).
- Remember filter_var itself only accepts Closures for FILTER_CALLBACK — never strings or arrays.
- Double-check the 'options' key spelling and that it is not overwritten elsewhere.
- Add a unit test for each custom callback filter to surface option-shape mistakes early.
When it happens
Trigger: Calling $inputBag->filter('key', FILTER_CALLBACK) without passing ['options' => fn(...) => ...]; passing a callable string like 'strtoupper' or a first-class callable syntax array [$obj, 'method'] as options instead of a Closure; passing ['options' => FILTER_VALIDATE_INT] by mistake.
Common situations: Developers assuming any PHP callable works with FILTER_CALLBACK (filter_var only accepts Closures for it); forgetting the 'options' key entirely; migrating code where a function name string was used with filter_var directly.
Understand the failure class
Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.
Related errors
- Expected a scalar, or an array as a 2nd argument to
- The "sameSite" parameter value is not valid.
- The cookie name " " uses a reserved prefix, which requires…
- The cookie name " " uses the "__Host-" prefix, which…
- The cookie name " " uses the "__Host-" prefix, which…
AI-assisted analysis of symfony/http-foundation@5aea19cd67 (2026-09-13).
Data as JSON: /api/errors/80a033cfdbbdea11.
Report an issue: GitHub.
Appendix: source
Thrown at InputBag.php:157
/**
* @throws BadRequestException if the input value is an array and \FILTER_REQUIRE_ARRAY or \FILTER_FORCE_ARRAY is not set
* @throws BadRequestException if the input value is invalid and \FILTER_NULL_ON_FAILURE is not set
*/
public function filter(string $key, mixed $default = null, int $filter = \FILTER_DEFAULT, mixed $options = []): mixed
{
$value = $this->has($key) ? $this->all()[$key] : $default;
// Always turn $options into an array - this allows filter_var option shortcuts.
if (!\is_array($options) && $options) {
$options = ['flags' => $options];
}
if (\is_array($value) && !(($options['flags'] ?? 0) & (\FILTER_REQUIRE_ARRAY | \FILTER_FORCE_ARRAY))) {
throw new BadRequestException(\sprintf('Input value "%s" contains an array, but "FILTER_REQUIRE_ARRAY" or "FILTER_FORCE_ARRAY" flags were not set.', $key));
}
if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) {
throw new \InvalidArgumentException(\sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null)));
}
$options['flags'] ??= 0;
$nullOnFailure = $options['flags'] & \FILTER_NULL_ON_FAILURE;
$options['flags'] |= \FILTER_NULL_ON_FAILURE;
$value = filter_var($value, $filter, $options);
if (null !== $value || $nullOnFailure) {
return $value;
}
throw new BadRequestException(\sprintf('Input value "%s" is invalid and flag "FILTER_NULL_ON_FAILURE" was not set.', $key));
}
}
View on GitHub (pinned to 5aea19cd67)