solophp / request-guard
Robust request validation & authorization layer for HTTP inputs with type-safe guards
v1.2.0
2025-04-22 13:00 UTC
Requires
- php: ^8.1
- psr/http-message: ^2.0
- solophp/validator: ^2.1
README
Robust request validation & authorization layer for HTTP inputs with type-safe guards
✨ Features
- Type-safe request validation
- Field mapping from custom input names/nested structures
- Built-in authorization checks
- Multi-stage processing pipeline (
preprocess
→validate
→postprocess
) - Custom error messages
- PSR-7 compatible
- Smart request data merging (POST body > GET params)
- GET query cleaning with exception support
- Immutable field definitions
🔗 Dependencies
- PSR-7 HTTP Message Interface (
psr/http-message
^2.0) - Solo Validator (
solophp/validator
^2.1)
📥 Installation
composer require solophp/request-guard
🚀 Quick Start
Define a Request Guard
namespace App\Requests; use Solo\RequestGuard; use Solo\RequestGuard\Field; class CreateArticleRequest extends RequestGuard { protected function fields(): array { return [ Field::for('author_email') ->mapFrom('meta.author.email') ->validate('required|email'), Field::for('title') ->validate('required|string|max:100') ->preprocess('trim'), Field::for('status') ->default('draft') ->validate('string|in:draft,published') ->postprocess(fn($v) => strtoupper($v)) ]; } protected function authorize(): bool { return $this->user()->can('create', Article::class); } }
Handle in Controller
namespace App\Controllers; use App\Requests\CreateArticleRequest; use Solo\RequestGuard\Exceptions\{ValidationException, AuthorizationException}; class ArticleController { public function store(ServerRequestInterface $request) { try { $data = (new CreateArticleRequest(new Validator()))->handle($request); Article::create($data); return response()->json(['success' => true], 201); } catch (ValidationException $e) { return response()->json(['errors' => $e->getErrors()], 422); } catch (AuthorizationException $e) { return response()->json(['message' => $e->getMessage()], 403); } } }
⚙️ Field Configuration
Method | Required? | Description |
---|---|---|
Field::for(string) |
Yes | Starts field definition |
mapFrom(string) |
No | Map input from custom name/nested path |
default(mixed) |
No | Fallback value if field is missing |
validate(string) |
No | Validation rules (e.g., `required |
preprocess(callable) |
No | Transform raw input before validation |
postprocess(callable) |
No | Modify value after validation |
Processing Pipeline
- Map Input - Resolve value using
mapFrom
path - Preprocess - Clean/transform raw input
- Validate - Check against rules
- Postprocess - Final value adjustments
Example
Field::for('tags') ->mapFrom('raw_csv') ->preprocess(fn($v) => explode(',', $v)) ->validate('array|max:5') ->postprocess(fn($v) => array_unique($v));
🔄 Request Data Handling
- Nested Structures: Use dot notation (
mapFrom('contacts.user_name')
) - GET: Query parameters only
- POST/PUT/PATCH: Merged body + query (body priority)
- Files: Via
$request->getUploadedFiles()
⚡ Error Handling
ValidationException (HTTP 422)
catch (ValidationException $e) { return ['errors' => $e->getErrors()]; // Format: ['field' => ['Error 1']] }
AuthorizationException (HTTP 403)
catch (AuthorizationException $e) { return ['message' => $e->getMessage()]; // "Unauthorized request" }
UncleanQueryException (HTTP 400 or custom)
catch (UncleanQueryException $e) { return redirect($e->getRedirectUri()); }
🚦 Custom Messages
protected function messages(): array { return [ 'author_email.required' => 'Author email required', 'status.in' => 'Invalid status: :value' ]; }
🛠️ Testing
public function test_nested_mapping() { $data = $request->handle( $this->createRequest('POST', '/', [ 'meta' => ['author' => ['email' => 'test@example.com']] ]) ); $this->assertEquals('test@example.com', $data['author_email']); }
📚 Public API
Method | Description |
---|---|
handle(ServerRequestInterface $request): array |
Main entry point: cleans, authorizes, validates, postprocesses |
getRequestData(ServerRequestInterface $request): array |
Merges GET/POST params, returns raw input |
removeDefaults(array $data): array |
Strips default values from the given array |
getDefaults(): array |
Returns all non-null default field values |
⚙️ Requirements
- PHP 8.1+
📄 License
MIT - See LICENSE for details.