{"record":{"id":"80a033cfdbbdea11","repo":"symfony/http-foundation","slug":"a-closure-must-be-passed-to-s-when-filter-callback-is-used-s","errorCode":null,"errorMessage":"A Closure must be passed to \"%s()\" when FILTER_CALLBACK is used, \"%s\" given.","messagePattern":"A Closure must be passed to \"(.+?)\\(\\)\" when FILTER_CALLBACK is used, \"(.+?)\" given\\.","errorType":"exception","errorClass":"InvalidArgumentException","httpStatus":null,"severity":"error","filePath":"InputBag.php","lineNumber":157,"sourceCode":"    /**\n     * @throws BadRequestException if the input value is an array and \\FILTER_REQUIRE_ARRAY or \\FILTER_FORCE_ARRAY is not set\n     * @throws BadRequestException if the input value is invalid and \\FILTER_NULL_ON_FAILURE is not set\n     */\n    public function filter(string $key, mixed $default = null, int $filter = \\FILTER_DEFAULT, mixed $options = []): mixed\n    {\n        $value = $this->has($key) ? $this->all()[$key] : $default;\n\n        // Always turn $options into an array - this allows filter_var option shortcuts.\n        if (!\\is_array($options) && $options) {\n            $options = ['flags' => $options];\n        }\n\n        if (\\is_array($value) && !(($options['flags'] ?? 0) & (\\FILTER_REQUIRE_ARRAY | \\FILTER_FORCE_ARRAY))) {\n            throw new BadRequestException(\\sprintf('Input value \"%s\" contains an array, but \"FILTER_REQUIRE_ARRAY\" or \"FILTER_FORCE_ARRAY\" flags were not set.', $key));\n        }\n\n        if ((\\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \\Closure)) {\n            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)));\n        }\n\n        $options['flags'] ??= 0;\n        $nullOnFailure = $options['flags'] & \\FILTER_NULL_ON_FAILURE;\n        $options['flags'] |= \\FILTER_NULL_ON_FAILURE;\n\n        $value = filter_var($value, $filter, $options);\n\n        if (null !== $value || $nullOnFailure) {\n            return $value;\n        }\n\n        throw new BadRequestException(\\sprintf('Input value \"%s\" is invalid and flag \"FILTER_NULL_ON_FAILURE\" was not set.', $key));\n    }\n}\n","sourceCodeStart":139,"sourceCodeEnd":173,"githubUrl":"https://github.com/symfony/http-foundation/blob/5aea19cd678fa4140f6108406f1096de5e9ed6e4/InputBag.php#L139-L173","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n$bag->filter('name', \\FILTER_CALLBACK, ['options' => 'trim']); // string callable: throws\n\n// after\n$bag->filter('name', \\FILTER_CALLBACK, ['options' => fn ($v) => trim($v)]);","handlingStrategy":"validation","validationCode":"// before calling filter with FILTER_CALLBACK\n$options = $options ?? [];\nif (\\FILTER_CALLBACK & $filter && !($options['options'] ?? null) instanceof \\Closure) {\n    $options['options'] = $options['options'](...); // wrap callable in a Closure\n}","typeGuard":"function isClosure(mixed $c): bool {\n    return $c instanceof \\Closure;\n}","tryCatchPattern":"try {\n    $value = $bag->filter($key, \\FILTER_CALLBACK, ['options' => $callback]);\n} catch (\\InvalidArgumentException $e) {\n    $value = null; // or fall back to an unfiltered default\n}","preventionTips":["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."],"tags":["php","symfony","filter-callback","input-validation"],"backgroundTag":"missing-required-argument","analyzedSha":"5aea19cd678fa4140f6108406f1096de5e9ed6e4","analyzedAt":"2026-09-13T01:52:22.855Z","contentChangedAt":"2026-09-13T01:52:22.855Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}