webman/validation
Laravel-style validation for the webman framework. Validate request data with expressive rule strings, custom messages, and scene-based rulesets, then reuse the same validator across controllers and services. Lightweight, framework-friendly, and easy to integrate into existing webman applications.
webman/validation is a thin integration layer between Webman and topthink/think-validate. It gives you three things Webman doesn't ship with: PHP-attribute validation on controller methods, named "scenes" (rule subsets per action), and a global exception handler that turns a failed check into an HTTP response instead of a manual if. If you're coming from Laravel, think of it as a stripped-down FormRequest + $request->validate() combo, with rules declared in dedicated validator classes rather than inline arrays.
Minimal steps:
composer require webman/validationcomposer require alone does nothing until php start.php restart -d (or start in dev).config/plugin/webman/validation/ (app/config + language files). Confirm the plugin is enabled and note the exception-handler class name — you'll likely override it later.vendor/webman/validation/src/Annotation/Validate.php. With 1 star and 0 dependents, the source is the spec.First use case — a login endpoint:
// app/validate/LoginValidator.php
namespace app\validate;
use think\Validate;
class LoginValidator extends Validate
{
protected $rule = [
'username' => 'require|alphaDash|length:4,32',
'password' => 'require|length:6,64',
];
protected $message = [
'username.require' => 'Username is required',
'username.length' => 'Username must be 4-32 characters',
];
protected $scene = [
'login' => ['username', 'password'],
'register' => ['username', 'password'], // swap in stricter rules later
];
}
// app/controller/AuthController.php
namespace app\controller;
use app\validate\LoginValidator;
use support\Request;
use Webman\Validation\Annotation\Validate;
class AuthController
{
#[Validate(LoginValidator::class, scene: 'login')]
public function login(Request $request)
{
$data = $request->post(); // already validated by the time you get here
// ... issue token
return json(['code' => 0, 'data' => ['token' => $token]]);
}
}
If the attribute doesn't fire (wrong middleware order, controller invoked from a closure, etc.), fall back to manual invocation — same validator, no magic:
$v = new LoginValidator();
if (!$v->scene('login')->check($request->post())) {
return json(['code' => 1, 'msg' => $v->getError()], 422);
}
One validator class per resource, one scene per action. This is the idiomatic shape. $rule holds the union of all fields; $scene picks the subset. Controllers stay thin, and the same validator is reusable from queue jobs, console commands, and service classes (where attributes never run).
Class-level attribute + per-method scene. Put #[Validate(UserValidator::class)] on the controller to apply a default, then override with #[Validate(UserValidator::class, scene: 'update')] on individual actions. Keeps boilerplate out of CRUD controllers.
Manual validation in the service layer. Attributes are an HTTP concern. Anywhere else — a Workerman timer, a queue consumer, an import command — instantiate and call check() yourself. Wrap it once:
function validate_or_fail(Validate $v, array $data, ?string $scene = null): array
{
if ($scene) $v->scene($scene);
if (!$v->batch(true)->check($data)) {
throw new \InvalidArgumentException($v->getError());
}
return array_intersect_key($data, $v->getRule()); // whitelist
}
Custom rules. think-validate exposes extend() for closures and rule methods on the validator (checkPhone, checkSkuExists, etc.). Register globally in config/bootstrap.php so every worker has them; register per-validator when the rule needs injected dependencies. Anything touching your DB layer is a custom rule — see gotchas.
Whitelisting input. Validation is checking, not filtering. check() does not strip unknown keys, and it does not mutate $request. If you want $request->post() to behave like Laravel's validated(), build the subset yourself (array_intersect_key($data, $v->getRule())) or use think-validate's only().
Consistent API error envelope. Don't leak think-validate's raw strings. Register a custom exception handler in config/exception.php that catches the plugin's validation exception and maps it to {code, msg, errors[]} with a 422. Parse $validator->error (the public array property) rather than getError() if you want field-level detail.
Localization. Messages come from the plugin's lang files, keyed field.rule ('username.require' => '...'). Set the locale per request in a middleware before validation runs (header, user preference, subdomain). Fallback locale lives in the plugin config.
Layering with Webman's upload/JSON handling. For JSON bodies, $request->post() already decodes. For multipart, mix $request->post() with $request->file(). For file rules, see gotchas — you'll write your own.
require ≠ Laravel required. think-validate's require rejects '', null, and [] but accepts 0 and '0'. That's usually what you want for numeric fields, but it will bite you when a 0-valued status passes require and then fails a between:1,10. Use require|integer|between:... deliberately.unique and exists are broken here. They're inherited from think-orm's Db facade. Webman with illuminate/database (the common setup) has no such facade — the rule either throws or silently no-ops. Write a custom rule against your actual DB connection.file / image / fileExt / fileSize expect think\File. Webman hands you Webman\Http\UploadFile (a Symfony UploadedFile). These rules will not behave. Use Symfony's API in a custom rule: isValid(), getClientOriginalExtension(), getSize(), getMimeType().getError() returns one string, not an array. With batch(true) it concatenates with <br> — a JSON-land disaster. Read the $validator->error property or explode('<br', ...), or better, write your own error extractor.scene('login ') or a scene key that drifted from the controller attribute means zero rules run and every request succeeds. Assert your scene exists in a test.'regex:/^\d{1,3}(,\d{3})*$/' gets split on the comma inside the pattern. Use the array form: ['regex', '/^\d{1,3}(,\d{3})*$/'].$this->error accumulates. Always new, never a singleton bound into the container.php start.php reload is not always enough for new plugin files. Restart in dev, and make sure your deploy script does too.* wildcards exist in recent think-validate, but support is inconsistent and untested in this plugin. For deeply nested JSON, either flatten to a.b.c keys yourself or write a per-element loop.scene()/message(); append()/remove() to adjust rules at runtime; extend() for global rules; swap the exception handler; swap the lang loader. The plugin is small enough that the source is readable in 20 minutes — do that before opening an issue.composer.json, and keep your custom rules in app/ (not in vendor/) so swapping to hand-rolled think\Validate calls — or to a Laravel-style validator — is a grep away rather than a rewrite.How can I help you explore Laravel packages today?