ViewModels are a design pattern used in Anchor to encapsulate presentation logic and data preparation for Views. Instead of passing raw arrays or complex models directly to a view, you pass a dedicated ViewModel object.
- Separation of Concerns: Moves presentation logic (formatting dates, checking errors, conditional classes) out of the View and Controller.
- Type Safety: ViewModels are classes with typed properties and methods, providing better IDE support than associative arrays.
- Testability: You can easily unit test a ViewModel without needing to render a view.
- Reusability: Shared presentation logic can be reused across different views.
A ViewModel is typically a simple PHP class (often readonly) that takes dependencies in its constructor and exposes public methods for the view.
Example ViewModel
App/src/Auth/Views/Models/LoginViewModel.php
<?php
namespace App\Auth\Views\Models;
use Helpers\Http\Flash;
readonly class LoginViewModel
{
public function __construct(
private Flash $flash
) {}
public function getPageTitle(): string
{
return 'Login';
}
public function getFormActionUrl(): string
{
return url('auth/login');
}
public function hasError(string $field): bool
{
return $this->flash->hasInputError($field);
}
public function getErrorClass(string $field): string
{
return $this->hasError($field) ? 'is-invalid' : '';
}
}You can quickly generate a ViewModel using the dock CLI:
php dock view:create-model <ModelName> <ModuleName>php dock view:create-model Login AuthThis will create App/src/Auth/Views/Models/LoginViewModel.php.
Use the --form (or -f) option to generate a ViewModel pre-populated with form helper methods:
php dock view:create-model Register Auth --formThe generated ViewModel will include methods for handling errors and persisting old input:
// Checks if a field has an error
public function hasError(string $field): bool
// Returns 'is-invalid' class if field has error
public function getErrorClass(string $field): string
// Returns the flash/old value for a field
public function getFieldValue(string $field): stringYou can inject the ViewModel directly into your controller method:
use App\Auth\Views\Models\LoginViewModel;
public function index(LoginViewModel $viewModel)
{
// ViewModel is automatically resolved and injected
return $this->asView('login', ['model' => $viewModel]);
}Or manually resolve it from the container:
// ...
$viewModel = resolve(LoginViewModel::class);
// ...Use the methods provided by the ViewModel.
<!-- App/src/Auth/Views/Templates/login.php -->
<h1><?php echo $model->getPageTitle(); ?></h1>
<form action="<?php echo $model->getFormActionUrl(); ?>" method="POST">
<div class="form-group">
<label>Email</label>
<input type="email" name="email" class="<?php echo $model->getErrorClass('email'); ?>">
</div>
</form>- Keep it Read-Only: ViewModels should generally be immutable data holders.
- Dependency Injection: Use the constructor to inject Services, Flash helpers, or Request objects.
- No Business Logic: ViewModels should only contain logic related to presentation (e.g., "is this button active?", "format this currency"), not core business rules.
- One per View: typically create one ViewModel per page (e.g.,
ProfileViewModel,LoginViewModel).