The Neuron CMS provides a robust request handling system that integrates Data Transfer Objects (DTOs) for validated, type-safe data processing. This integration allows controllers to define request schemas in YAML configuration files, automatically populate DTOs from incoming requests, and perform comprehensive validation before processing.
The request/DTO system provides:
The request/DTO system consists of the following classes:
Neuron\Mvc\Requests\Request): Request object with parameter access, DTO integration, and validationNeuron\Dto\Dto): Data Transfer Object with properties and validationNeuron\Dto\Property): Individual DTO property with type and validation rulesNeuron\Dto\Factory): Creates DTOs from YAML configuration or arraysNeuron\Cms\Controllers\Traits\UsesDtos): Controller helper methods for DTO operationsThe typical request processing flow:
The Neuron\Mvc\Requests\Request class provides filtered access to request data:
use Neuron\Mvc\Requests\Request;
$request = new Request();
// GET parameters (filtered)
$page = $request->get( 'page', 1 );
$search = $request->get( 'search' );
// POST parameters (filtered)
$username = $request->post( 'username' );
$email = $request->post( 'email' );
// SERVER parameters
$userAgent = $request->server( 'HTTP_USER_AGENT' );
$method = $request->server( 'REQUEST_METHOD' );
// SESSION parameters
$userId = $request->session( 'user_id' );
// COOKIE parameters
$token = $request->cookie( 'remember_token' );
// Client IP address
$clientIp = $request->getClientIp();
// JSON payload
$payload = $request->getJsonPayload();
Route parameters extracted from URL patterns:
// Route: /posts/:id/comments/:comment_id
$postId = $request->getRouteParameter( 'id' );
$commentId = $request->getRouteParameter( 'comment_id' );
// All route parameters
$params = $request->getRouteParameters();
use Neuron\Routing\RequestMethod;
$method = $request->getRequestMethod();
if( $method === RequestMethod::POST )
{
// Handle POST request
}
Request schemas are defined in YAML files with inline DTO properties:
request:
method: POST
headers:
Content-Type: application/json
properties:
propertyName:
type: string|integer|boolean|array|object
required: true|false
# Additional validation rules
Create config/requests/login.yaml:
request:
method: POST
headers:
Content-Type: application/json
properties:
username:
required: true
type: string
length:
min: 3
max: 20
password:
required: true
type: string
length:
min: 8
max: 50
remember_me:
type: boolean
Request schemas support nested DTOs:
request:
method: POST
properties:
username:
required: true
type: string
length:
min: 3
max: 20
email:
required: true
type: email
address:
required: true
type: object
properties:
street:
required: true
type: string
length:
min: 5
max: 100
city:
required: true
type: string
state:
required: true
type: string
length:
min: 2
max: 2
zip:
required: true
type: string
pattern: '/^\d{5}(-\d{4})?$/'
request:
method: POST
properties:
user:
type: object
required: true
properties:
name:
type: string
required: true
email:
type: email
required: true
phone_numbers:
type: array
items:
type: object
properties:
type:
type: string
enum: ['home', 'work', 'mobile']
required: true
number:
type: string
pattern: '/^\+?[\d\s\-\(\)]+$/'
required: true
use Neuron\Mvc\Requests\Request;
$request = new Request();
$request->loadFile( 'config/requests/login.yaml' );
// Access configured DTO
$dto = $request->getDto();
// Process payload against DTO
$payload = [
'username' => 'johndoe',
'password' => 'securepass123',
'remember_me' => true
];
$request->processPayload( $payload );
The Request class automatically populates DTOs from request data:
// Request with inline DTO
$request = new Request();
$request->loadFile( 'config/requests/registration.yaml' );
// Populate from POST data
$request->processPayload( $_POST );
// Access populated DTO
$dto = $request->getDto();
echo $dto->username; // 'johndoe'
echo $dto->email; // '[email protected]'
echo $dto->address->street; // '123 Main St'
DTOs can be created programmatically without YAML:
use Neuron\Dto\Factory;
// Create DTO from array definition
$factory = new Factory( [
'username' => [
'type' => 'string',
'required' => true,
'length' => ['min' => 3, 'max' => 20]
],
'email' => [
'type' => 'email',
'required' => true
],
'age' => [
'type' => 'integer',
'range' => ['min' => 18, 'max' => 120]
]
] );
$dto = $factory->create();
// Set values
$dto->username = 'johndoe';
$dto->email = '[email protected]';
$dto->age = 30;
// Validate
try {
$dto->validate();
} catch( \Neuron\Core\Exceptions\Validation $e )
{
$errors = $e->errors;
}
DTOs validate automatically when processPayload() is called:
try {
$request->processPayload( $_POST );
// Validation passed - safe to use DTO
$dto = $request->getDto();
} catch( \Neuron\Core\Exceptions\Validation $e )
{
// Validation failed
$errors = $request->getErrors();
foreach( $errors as $error )
{
echo "Error: $error\n";
}
}
The UsesDtos trait provides helper methods for DTO operations in controllers:
use Neuron\Cms\Controllers\Traits\UsesDtos;
use Neuron\Mvc\Controller;
class RegistrationController extends Controller
{
use UsesDtos;
// Trait methods now available
}
// Get DTO factory service
protected function getDtoFactory(): DtoFactoryService
// Populate DTO from request POST data
protected function populateDtoFromRequest( Dto $dto,
Request $request,
array $fields = [] ): Dto
// Validate DTO and return errors (empty if valid)
protected function validateDto( Dto $dto ): array
// Validate DTO and throw exception if invalid
protected function validateDtoOrFail( Dto $dto ): void
// Create and populate DTO from request in one step
protected function createDtoFromRequest( string $name,
Request $request,
array $fields = [] ): Dto
The DTO factory service looks for DTO YAML files in standard locations:
// Expects config/dtos/RegisterUser.yaml
$dto = $this->getDtoFactory()->create( 'RegisterUser' );
// Populate from request
$dto = $this->populateDtoFromRequest( $dto, $request );
// Or create and populate in one step
$dto = $this->createDtoFromRequest( 'RegisterUser', $request );
DTOs are stored in config/dtos/ directory:
config/
├── dtos/
│ ├── RegisterUser.yaml
│ ├── LoginUser.yaml
│ ├── CreatePost.yaml
│ ├── UpdateProfile.yaml
│ └── ContactForm.yaml
└── requests/
├── login.yaml
└── registration.yaml
DTO configuration (config/dtos/LoginUser.yaml):
dto:
username:
type: string
required: true
length:
min: 3
max: 50
password:
type: string
required: true
length:
min: 8
max: 255
remember_me:
type: boolean
Controller:
namespace App\Controllers;
use Neuron\Cms\Controllers\Content;
use Neuron\Cms\Controllers\Traits\UsesDtos;
use Neuron\Mvc\Requests\Request;
class LoginController extends Content
{
use UsesDtos;
public function processLogin( Request $request ): never
{
try {
// Create and populate DTO from request
$dto = $this->createDtoFromRequest( 'LoginUser', $request );
// Validate DTO (throws exception if invalid)
$this->validateDtoOrFail( $dto );
// Use validated data
$authenticated = $this->authManager->login( $dto->username,
$dto->password,
$dto->remember_me ?? false );
if( $authenticated )
{
$this->redirect( 'dashboard' );
}
else {
$this->redirect( 'login', [], ['error', 'Invalid credentials'] );
}
} catch( \Exception $e )
{
$this->redirect( 'login', [], ['error', $e->getMessage()]);
}
}
}
DTO configuration (config/dtos/RegisterUser.yaml):
dto:
username:
type: string
required: true
length:
min: 3
max: 20
pattern: '/^[a-zA-Z0-9_]+$/'
email:
type: email
required: true
password:
type: string
required: true
length:
min: 8
max: 255
password_confirmation:
type: string
required: true
profile:
type: object
required: true
properties:
first_name:
type: string
required: true
length:
min: 2
max: 50
last_name:
type: string
required: true
length:
min: 2
max: 50
birth_date:
type: date
phone:
type: string
pattern: '/^\+?[\d\s\-\(\)]+$/'
address:
type: object
properties:
street:
type: string
length:
min: 5
max: 100
city:
type: string
state:
type: string
length:
min: 2
max: 2
zip:
type: string
pattern: '/^\d{5}(-\d{4})?$/'
Controller:
public function processRegistration( Request $request ): never
{
try {
// Create and populate DTO
$dto = $this->createDtoFromRequest( 'RegisterUser', $request );
// Validate DTO
$this->validateDtoOrFail( $dto );
// Additional custom validation
if( $dto->password !== $dto->password_confirmation )
{
throw new \Exception( 'Passwords do not match' );
}
// Create user account
$user = $this->registrationService->register( $dto->username,
$dto->email,
$dto->password );
// Update profile with nested data
$user->setFirstName( $dto->profile->first_name );
$user->setLastName( $dto->profile->last_name );
if( $dto->address )
{
$user->setAddress( [
'street' => $dto->address->street,
'city' => $dto->address->city,
'state' => $dto->address->state,
'zip' => $dto->address->zip
] );
}
$this->userRepository->update( $user );
$this->redirect( 'verify_email_sent', [], [
'success',
'Registration successful! Please check your email.'
] );
} catch( \Exception $e )
{
$this->redirect( 'register', [], ['error', $e->getMessage()]);
}
}
Request configuration (config/requests/create-post.yaml):
request:
method: POST
headers:
Content-Type: application/json
properties:
title:
type: string
required: true
length:
min: 5
max: 200
content:
type: string
required: true
length:
min: 50
excerpt:
type: string
length:
max: 500
status:
type: string
enum: ['draft', 'published', 'scheduled']
required: true
category_ids:
type: array
items:
type: integer
tags:
type: array
items:
type: string
length:
min: 2
max: 30
Controller:
namespace App\Controllers\Api;
use Neuron\Mvc\Controller;
use Neuron\Mvc\Requests\Request;
class PostsController extends Controller
{
public function create( Request $request ): string
{
try {
// Load request configuration with DTO
$request->loadFile( 'config/requests/create-post.yaml' );
// Get JSON payload
$payload = $request->getJsonPayload();
// Process and validate against DTO
$request->processPayload( $payload );
// Access validated DTO
$dto = $request->getDto();
// Create post
$post = $this->postService->create( $dto->title,
$dto->content,
$dto->status,
$dto->excerpt ?? null );
// Attach categories and tags
if (isset( $dto->category_ids )) {
$this->postService->attachCategories( $post, $dto->category_ids );
}
if (isset( $dto->tags )) {
$this->postService->attachTags( $post, $dto->tags );
}
return $this->renderJson( ['success' => true, 'post' => $post->toArray()],
201 );
} catch( \Neuron\Core\Exceptions\Validation $e )
{
return $this->renderJson( ['success' => false, 'errors' => $e->errors],
422 );
} catch( \Exception $e )
{
return $this->renderJson( ['success' => false, 'error' => $e->getMessage()],
500 );
}
}
}
View template (HTML form with error display):
<form method="POST" action="/register">
<?= csrf_field() ?>
<?php if ($Error = $this->get( 'Error' )): ?>
<div class="alert alert-danger">
<?= htmlspecialchars( $Error ) ?>
</div>
<?php endif; ?>
<div class="form-group">
<label>Username</label>
<input type="text" name="username"
value="<?= htmlspecialchars( $_POST['username'] ?? '' ) ?>"
class="form-control" required>
</div>
<div class="form-group">
<label>Email</label>
<input type="email" name="email"
value="<?= htmlspecialchars( $_POST['email'] ?? '' ) ?>"
class="form-control" required>
</div>
<div class="form-group">
<label>Password</label>
<input type="password" name="password"
class="form-control" required>
</div>
<button type="submit" class="btn btn-primary">Register</button>
</form>
Controller with detailed error handling:
public function processRegistration( Request $request ): never
{
try {
// Create and populate DTO
$dto = $this->createDtoFromRequest( 'RegisterUser', $request );
// Get validation errors (non-throwing)
$errors = $this->validateDto( $dto );
if (!empty( $errors )) {
// Format errors for display
$errorMessage = implode( ' ', array_map(function($error ) {
return ucfirst( $error );
}, $errors));
$this->redirect( 'register', [], ['error', $errorMessage] );
}
// Process registration
$this->registrationService->registerWithDto( $dto );
$this->redirect( 'verify_email_sent', [], [
'success',
'Registration successful! Please check your email.'
] );
} catch( \Exception $e )
{
$this->redirect( 'register', [], ['error', $e->getMessage()]);
}
}
DTOs support comprehensive type validation:
| Type | Description | Validation |
|---|---|---|
string |
Text values | Length, pattern |
integer |
Whole numbers | Range, min, max |
float |
Decimal numbers | Range, precision |
boolean |
True/false values | Type checking |
array |
Lists of items | Item validation |
object |
Nested objects | Property validation |
email |
Email addresses | RFC compliance |
url |
URLs | URL format |
date |
Date values | Date format |
date_time |
Date and time | DateTime format |
time |
Time values | Time format |
currency |
Money amounts | Currency format |
uuid |
UUIDs | UUID v4 format |
ip_address |
IP addresses | IPv4/IPv6 |
us_phone_number |
US phone numbers | US format |
intl_phone_number |
International phones | International format |
Length validation:
username:
type: string
length:
min: 3
max: 20
Range validation:
age:
type: integer
range:
min: 18
max: 120
Pattern validation:
zipcode:
type: string
pattern: '/^\d{5}(-\d{4})?$/'
Enum validation:
status:
type: string
enum: ['active', 'inactive', 'pending']
Validation errors are collected and accessible:
try {
$dto->validate();
} catch( \Neuron\Core\Exceptions\Validation $e )
{
// Exception contains errors array
$errors = $e->errors;
// Example errors:
// [
// 'username: length validation failed.',
// 'email: email validation failed.',
// 'age: range validation failed.'
// ]
}
// Or from request
$request->processPayload( $_POST );
$errors = $request->getErrors();
Nested DTOs validate recursively:
// Validates all properties including nested address object
$dto->validate();
// Errors from nested objects are included:
// [
// 'username: length validation failed.',
// 'address.street: length validation failed.',
// 'address.zip: pattern validation failed.'
// ]
Always validate CSRF tokens for state-changing requests:
use Neuron\Cms\Services\Auth\CsrfToken;
public function processForm( Request $request ): never
{
// Get CSRF token from request
$token = $request->post( 'csrf_token' );
// Validate token
$csrfToken = new CsrfToken( $this->getSessionManager());
if (!$csrfToken->validate( $token )) {
$this->redirect( 'form', [], ['error', 'Invalid CSRF token'] );
}
// Process form...
}
The Request class filters all input automatically:
// All methods use filtered input
$username = $request->post( 'username' ); // Filtered
$email = $request->get( 'email' ); // Filtered
// Manual filtering if needed
$raw = $_POST['data'];
$filtered = filter_var( $raw, FILTER_SANITIZE_STRING );
Always validate before using data:
// Good: Validate first
$dto = $this->createDtoFromRequest( 'CreatePost', $request );
$this->validateDtoOrFail( $dto );
$this->postService->create( $dto );
// Bad: Using data without validation
$dto = $this->createDtoFromRequest( 'CreatePost', $request );
$this->postService->create( $dto ); // Dangerous!
Prevent user enumeration by returning generic messages:
public function resendVerification( Request $request ): never
{
$email = $request->post( 'email' );
try {
$this->emailVerifier->resendVerification( $email );
} catch( \Exception $e )
{
// Don't reveal if email exists
}
// Always show same message
$this->redirect( 'verify_sent', [], [
'success',
'If an account exists, a verification email has been sent.'
] );
}
Additional password validation beyond DTO:
public function processRegistration( Request $request ): never
{
$dto = $this->createDtoFromRequest( 'RegisterUser', $request );
$this->validateDtoOrFail( $dto );
// Additional password checks
if( $dto->password !== $dto->password_confirmation )
{
throw new \Exception( 'Passwords do not match' );
}
if (strlen( $dto->password ) < 12) {
throw new \Exception( 'Password must be at least 12 characters' );
}
// Check password strength
if (!preg_match( '/[A-Z]/', $dto->password ) ||
!preg_match( '/[a-z]/', $dto->password ) ||
!preg_match( '/[0-9]/', $dto->password )) {
throw new \Exception( 'Password must contain uppercase, lowercase, and numbers' );
}
// Process registration...
}
Organize DTOs by functional area:
config/dtos/
├── auth/
│ ├── Login.yaml
│ ├── Register.yaml
│ └── ResetPassword.yaml
├── posts/
│ ├── CreatePost.yaml
│ ├── UpdatePost.yaml
│ └── PublishPost.yaml
├── users/
│ ├── CreateUser.yaml
│ ├── UpdateUser.yaml
│ └── UpdateProfile.yaml
└── contact/
└── ContactForm.yaml
Define common structures for reuse:
# config/dtos/fragments/address.yaml
dto:
street:
type: string
required: true
length:
min: 5
max: 100
city:
type: string
required: true
state:
type: string
length:
min: 2
max: 2
zip:
type: string
pattern: '/^\d{5}(-\d{4})?$/'
Provide clear validation messages:
public function processForm( Request $request ): never
{
try {
$dto = $this->createDtoFromRequest( 'ContactForm', $request );
$this->validateDtoOrFail( $dto );
} catch( \Exception $e )
{
// Transform technical errors to user-friendly messages
$message = $this->formatValidationMessage( $e->getMessage());
$this->redirect( 'contact', [], ['error', $message] );
}
}
private function formatValidationMessage( string $error ): string
{
$messages = [
'email validation failed' => 'Please enter a valid email address',
'length validation failed' => 'Input is too short or too long',
'value is required' => 'This field is required'
];
foreach( $messages as $pattern => $friendly )
{
if (str_contains( $error, $pattern )) {
return $friendly;
}
}
return 'Please check your input and try again';
}
Use consistent error handling patterns:
trait HandlesFormErrors
{
protected function handleFormValidation( string $dtoName,
Request $request,
string $redirectRoute ): Dto {
try {
$dto = $this->createDtoFromRequest( $dtoName, $request );
$this->validateDtoOrFail( $dto );
return $dto;
} catch( \Exception $e )
{
$this->redirect( $redirectRoute, [], ['error', $e->getMessage()]);
}
}
}
Test DTO validation thoroughly:
use PHPUnit\Framework\TestCase;
use Neuron\Dto\Factory;
class RegisterUserDtoTest extends TestCase
{
public function testValidData(): void
{
$factory = new Factory( 'config/dtos/RegisterUser.yaml' );
$dto = $factory->create();
$dto->username = 'johndoe';
$dto->email = '[email protected]';
$dto->password = 'SecurePass123!';
$dto->validate();
$this->assertEmpty( $dto->getErrors());
}
public function testInvalidUsername(): void
{
$factory = new Factory( 'config/dtos/RegisterUser.yaml' );
$dto = $factory->create();
$dto->username = 'ab'; // Too short
$dto->email = '[email protected]';
$dto->password = 'SecurePass123!';
$this->expectException( \Neuron\Core\Exceptions\Validation::class );
$dto->validate();
}
public function testNestedValidation(): void
{
$factory = new Factory( 'config/dtos/RegisterUser.yaml' );
$dto = $factory->create();
$dto->username = 'johndoe';
$dto->email = '[email protected]';
$dto->password = 'SecurePass123!';
// Invalid nested address
$dto->address->zip = '123'; // Too short
$this->expectException( \Neuron\Core\Exceptions\Validation::class );
$dto->validate();
}
}
Test controllers with DTO validation:
use PHPUnit\Framework\TestCase;
use Neuron\Mvc\Requests\Request;
class RegistrationControllerTest extends TestCase
{
public function testSuccessfulRegistration(): void
{
$controller = new RegistrationController();
$request = new Request();
// Mock POST data
$_POST = [
'username' => 'johndoe',
'email' => '[email protected]',
'password' => 'SecurePass123!',
'password_confirmation' => 'SecurePass123!'
];
// Process registration
$controller->processRegistration( $request );
// Assert redirect to success page
$this->assertEquals( 'verify_email_sent', $controller->getRedirectRoute());
}
public function testValidationFailure(): void
{
$controller = new RegistrationController();
$request = new Request();
// Mock invalid POST data
$_POST = [
'username' => 'ab', // Too short
'email' => 'invalid-email',
'password' => 'short'
];
// Process registration
$controller->processRegistration( $request );
// Assert redirect to error page
$this->assertEquals( 'register', $controller->getRedirectRoute());
$this->assertNotEmpty( $controller->getFlashMessage('error' ));
}
}
Test request/DTO integration:
public function testRequestDtoIntegration(): void
{
$request = new Request();
$request->loadFile( 'config/requests/login.yaml' );
$payload = [
'username' => 'johndoe',
'password' => 'SecurePass123!'
];
$request->processPayload( $payload );
$dto = $request->getDto();
$this->assertEquals( 'johndoe', $dto->username );
$this->assertEquals( 'SecurePass123!', $dto->password );
$this->assertEmpty( $request->getErrors());
}
Mock DTO factory in tests:
use PHPUnit\Framework\TestCase;
class UserServiceTest extends TestCase
{
public function testCreateUser(): void
{
// Create mock DTO
$dto = $this->createMock( \Neuron\Dto\Dto::class );
$dto->username = 'johndoe';
$dto->email = '[email protected]';
// Mock factory
$factory = $this->createMock( DtoFactoryService::class );
$factory->method( 'create' )->willReturn( $dto );
// Inject into service
$service = new UserService( $factory, $this->userRepository );
// Test service method
$user = $service->createFromDto( $dto );
$this->assertEquals( 'johndoe', $user->getUsername());
}
}