solophp/request-guard

Robust request validation & authorization layer for HTTP inputs with type-safe guards

v1.2.0 2025-02-27 16:31 UTC

This package is auto-updated.

Last update: 2025-02-27 16:33:08 UTC


README

Version PHP Version License

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 (preprocessvalidatepostprocess)
  • Custom error messages
  • PSR-7 compatible
  • Smart request data merging (POST body > GET params)
  • Immutable field definitions
  • Default values access via unified API

🔗 Dependencies

📥 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);
        }
    }
}

Working with Default Values

$guard = new CreateArticleRequest(new Validator());

// Get all defined default values
$defaults = $guard->defaultValues(); 
// Returns: ['status' => 'draft']

// Use defaults with form initialization
return view('articles.create', [
    'defaults' => $guard->defaultValues()
]);

⚙️ 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

  1. Map Input - Resolve value using mapFrom path
  2. Preprocess - Clean/transform raw input
  3. Validate - Check against rules
  4. 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()
  • Defaults: Access via $guard->defaultValues()

⚡ 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"
}

🚦 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']);
}

⚙️ Requirements

  • PHP 8.2+

📄 License

MIT - See LICENSE for details.