dotkernel-input-validation
Use when handling incoming data in a Dotkernel project — writing or changing an InputFilter, an Input class, or a laminas-form Form; wiring validators, filters and error messages; handling CSRF on a t
SKILL.md.claude/skills/dotkernel-input-validation/SKILL.md
GitHubInput validation and forms
Applies to
Projects: - Dotkernel API
- Dotkernel Admin
- Dotkernel Frontend
Packages: - none
Never trust input. Nothing from the request reaches a service, entity or query builder before it has been through validation.
Which mechanism depends on the variant (dotkernel-project-variants):
| Variant | Mechanism |
|---|---|
| API | laminas/laminas-inputfilter |
| Admin / Frontend | laminas/laminas-form — objects for elements, wrapping an InputFilter, plus CSRF |
The stock Light accepts no input and the stock Queue validates no request data; if you add either to one of them, use an InputFilter.
InputFilters (every variant)
src/<Module>/src/InputFilter/
├── Input/TitleInput.php extends Laminas\InputFilter\Input
├── CreateBookInputFilter.php extends the repo's AbstractInputFilter
└── UpdateBookInputFilter.php
An Input class owns one field, and attaches both a filter chain and a validator chain:
class TitleInput extends Input
{
public const TITLE_MAX_LENGTH = 191;
public function __construct(?string $name = null, bool $isRequired = true)
{
parent::__construct($name);
$this->setRequired($isRequired);
$this->getFilterChain()
->attachByName(StringTrim::class)
->attachByName(StripTags::class);
$this->getValidatorChain()
->attachByName(NotEmpty::class, ['message' => Message::VALIDATOR_REQUIRED_FIELD], true)
->attachByName(StringLength::class, [
'max' => self::TITLE_MAX_LENGTH,
'message' => Message::validatorLengthMax(self::TITLE_MAX_LENGTH),
], true);
}
}
Rules:
- Reuse the shared Input classes in the project's
Appmodule (API and Admin: email, password, UUID, image, identity…) before writing a new one. maxlength must match the entity's column length. Drifting these two is a 500 waiting for a long input.- Messages come from the shared message catalogue constant, never inline strings.
- The
truethird argument breaks the chain on failure, so one field yields one message. - Nested structures use
CollectionInputFilter/ a child InputFilter added under a key. - Document the validated shape with
@phpstan-typeon the filter so phpstan can narrow it.
Using an InputFilter (API style)
$this->inputFilter->setData((array) $request->getParsedBody());
if (! $this->inputFilter->isValid()) {
throw BadRequestException::create(
detail: Message::VALIDATOR_INVALID_DATA,
additional: ['errors' => $this->inputFilter->getMessages()]
);
}
$data = (array) $this->inputFilter->getValues(); // never getRawValues()
getValues() returns filtered values; getRawValues() bypasses the filter chain and defeats
the point.
Forms (Admin / Frontend)
laminas-form gives a thin object layer over elements, an InputFilter per input, and binding
data to and from the form. It integrates with the Laminas security ecosystem —
laminas-escaper, laminas-validator, laminas-filter, laminas-session — so validation,
filtering and rendering enforce escaping by design.
- One
Formclass per use case, insrc/<Module>/src/Form/. - Attach the InputFilter rather than declaring validators inline, so the same rules are reusable from a CLI command or an API.
- Every form needs a CSRF element, and the handler must validate it. A form without CSRF is a vulnerability, not a simplification.
- On failure, re-render the same template with the form bound so the user sees their input and the messages — do not redirect and lose them.
- Escaping is the view layer's job; never build HTML in a handler or service.
Query parameters are input too
Sort, filter, pagination and search values arrive unvalidated. Whitelist sortable and
filterable columns against an explicit array before they reach a query builder, and clamp
limit to a maximum. An unwhitelisted sort is an injection and enumeration risk, not a style
detail.
Uploads
Validate MIME type and size with the shared image/file Input, store outside any web-executable path, and never trust the client-supplied filename or extension.
Checklist
- Every write path validates before any business logic runs.
- Input
maxlengths match the entity column lengths. - Messages come from the shared catalogue.
-
getValues(), notgetRawValues(). - CSRF element present and validated (templated projects).
- Failed form re-renders bound, with messages.
- Sort/filter/limit whitelisted and clamped.
- Uploads checked for type and size.
- Validated shape typed for phpstan.