Forms Example
Open the live demo at /documentation/examples/run/forms.
This example demonstrates named server-rendered forms on the DotApp kernel — written from scratch, with no extra Composer stack. The live page is plain HTML, posts to the same URL, and does not load dotapp.js.
Introduction
Three forms can share one POST endpoint when each form contains a {{ formName(Name) }} token. The controller calls $request->form(['POST'], 'Name', $ok, $err) for each expected form name and returns the rendered page for the form that matches.
Review the module and routing basics first if you are new to DotApp:
Creating the Examples Module
Create the Examples module with the DotApper CLI:
php dotapper.php --create-module=Examples
The live module activates only for the example runner URLs. In /app/modules/Examples/module.init.php, keep the route scope explicit:
public function initializeRoutes()
{
return ['/documentation/examples/run', '/documentation/examples/run/*'];
}
Creating the Forms Controller
Create a controller named Forms for the Examples module:
php dotapper.php --module=Examples --create-controller=Forms
Controllers in DotApp 2.0 expose public static action methods and are referenced with module controller strings such as 'Examples:Forms@index!'.
Configuring Routes
Define a route pair for both the slash and no-slash URL. The live module uses a small helper so GET and POST stay consistent:
public function initialize($dotApp)
{
Config::module('Examples', 'prefix') ?? Config::module('Examples', 'prefix', '/documentation/examples/run');
$p = rtrim((string) Config::module('Examples', 'prefix'), '/');
$pair = function (string $path): array {
$path = rtrim($path, '/');
return [$path, $path . '/'];
};
Router::get($pair($p . '/forms'), 'Examples:Forms@index!', Router::STATIC_ROUTE);
Router::post($pair($p . '/forms'), 'Examples:Forms@submit!', Router::STATIC_ROUTE);
}
The GET action renders the form page. The POST action checks the submitted form name and returns a fresh HTML response.
Creating the View
The live demo uses a standalone view at /app/modules/Examples/views/forms.view.php. The file is a complete HTML document.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ var: $title }} - DotApp PHP Framework 2.0</title>
<link rel="stylesheet" href="/assets/modules/Examples/css/examples.css" />
</head>
<body class="ex-body">
<main class="ex-main">
<h1>Named forms</h1>
<p>Three forms post to the same URL. This demo does not use dotapp.js.</p>
{{ if $formNumber }}
<div class="ex-status">Using form {{ var: $formNumber }}, you submitted the text: {{ var: $formText }}</div>
{{ /if }}
<form method="POST">
<input type="text" name="textfrom1" placeholder="Enter text to display" />
{{ formName(Form1) }}
<button type="submit">{{ var: $btnName }}</button>
</form>
<form method="POST">
<input type="text" name="textfromanother" placeholder="Enter text to display" />
{{ formName(Form2) }}
<button type="submit">{{ var: $btnName }}</button>
</form>
<form method="POST">
<input type="text" name="textfromanother" placeholder="Enter text to display" />
{{ formName(Form3) }}
<button type="submit">{{ var: $btnName }}</button>
</form>
</main>
</body>
</html>
The {{ formName(Form1) }}, {{ formName(Form2) }}, and {{ formName(Form3) }} tags must be inside their corresponding <form> tags.
Handling Forms
The live controller returns the HTML string. Each $request->form() call includes both a success callback and an error callback so non-matching form checks can safely continue.
use Dotsystems\App\Parts\Logger;
use Dotsystems\App\Parts\Renderer;
use Dotsystems\App\Parts\Response;
class Forms extends \Dotsystems\App\Parts\Controller
{
public static function index($request)
{
return self::formPage('', 0);
}
public static function submit($request)
{
$attempts = [
1 => ['Form1', 'textfrom1'],
2 => ['Form2', 'textfromanother'],
3 => ['Form3', 'textfromanother'],
];
foreach ($attempts as $num => $spec) {
$html = $request->form(['POST'], $spec[0], function ($request) use ($num, $spec) {
$text = (string) ($request->data()[$spec[1]] ?? '');
return self::formPage($text, $num);
}, function () {
return null;
});
if (is_string($html) && $html !== '') {
return $html;
}
}
return self::formPage('', 0);
}
private static function formPage(string $text, int $formNumber)
{
return self::view('forms', [
'title' => 'Named forms demo',
'docsUrl' => '/documentation/examples/forms',
'btnName' => 'Send',
'formNumber' => $formNumber,
'formText' => htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
]);
}
private static function view(string $name, array $vars)
{
$r = Renderer::new()->module('Examples')->setView($name, 'clean');
foreach ($vars as $key => $value) {
$r->setViewVar($key, $value);
}
$html = $r->renderView();
if ($html === '') {
Logger::use()->error('Examples view empty', ['view' => $name]);
return new Response(500, 'Template error');
}
return $html;
}
}
Renderer::new()->module('Examples')->setView('forms', 'clean') selects the standalone view before any variables are assigned with setViewVar().
Rendering Theory
DotApp can render a full view directly or render a view that contains {{ content }} and a layout. This live example uses the direct standalone-view approach because the demo page is self-contained.
$r = Renderer::new()->module('Examples')->setView('forms', 'clean');
$r->setViewVar('btnName', 'Send');
$html = $r->renderView();
Layout rendering applies when a shared wrapper is useful. This walkthrough uses the standalone forms.view.php page.
Live Demo
Try the live demo at /documentation/examples/run/forms. It posts three named forms to one endpoint and returns the rendered page from the controller.