Views & Responses
Every endpoint can serve HTML or JSON from the same method — the framework decides based on the request's Accept header.
View Files
Views are plain PHP files stored in modules/MyModule/view/. They have full access to $this — the module instance — so any properties you set on the controller are available directly in the template:
<?php
namespace Zero\Module;
class MyModule extends \Zero\Core\Module {
public function index() {
$this->title = 'Dashboard';
$this->items = ['Alpha', 'Bravo', 'Charlie'];
$this->respond($this->viewPath . 'index.php');
}
}
<h1><?= $this->title ?></h1>
<ul>
<?php foreach ($this->items as $item): ?>
<li><?= htmlspecialchars($item) ?></li>
<?php endforeach; ?>
</ul>
View files are loaded with include inside the Response class, which is why $this refers to the module instance. There is no separate template engine — it is just PHP.
Response Methods
The module's base class provides several methods for sending responses:
| Method | Purpose | Behavior |
|---|---|---|
respond($view) |
Primary response method | JSON request → exports $this->dataHTML request → renders the view file with frame |
build($view) |
Low-level HTML render | Renders view wrapped in frame (head, header, footer, sideNav). Skipped for JSON requests. |
export($data, $extra) |
Low-level JSON output | Wraps data in the JSON envelope and outputs with Content-Type: application/json |
success($msg, $data) |
Success after mutation | JSON request → exports and exits HTML request → returns (so redirect can follow) |
error($msg, $data) |
Error after mutation | JSON request → sends error code and exits HTML request → returns (so form can re-render) |
JSON Response Envelope
All JSON responses from Zero are wrapped in a standard envelope. This applies to respond(), export(), success(), and error():
{
"data": { ... },
"status": "success",
"code": 200
}
When using respond() with a dual-purpose endpoint, $this->data is wrapped inside the envelope's "data" key — it is not at the root of the JSON response. Always unwrap the envelope client-side.
When $data is a string instead of an array, it becomes a "message" key instead of "data":
{
"message": "Article created successfully",
"status": "success",
"code": 200
}
Dual HTML/JSON Endpoints
The most powerful pattern in Zero is the dual-purpose endpoint. A single method serves both an HTML page and a JSON API depending on the request's Accept header:
public function index() {
// Set $this->data — this is what JSON clients receive
$this->data = [
'articles' => $this->model->getAll(),
'count' => $this->model->count(),
];
// respond() checks the Accept header:
// JSON → exports $this->data as {"data": {"articles": [...], "count": 5}, ...}
// HTML → renders the view (which can also use $this->data)
$this->respond($this->viewPath . 'index.php');
}
Client-side, unwrap the envelope to access your data:
fetch('/article', {
headers: { 'Accept': 'application/json' }
})
.then(r => r.json())
.then(res => {
// res.data contains your $this->data payload
// res.status and res.code are envelope metadata
const articles = res.data.articles;
const count = res.data.count;
});
The Frame System
When build() renders an HTML response, it wraps the view inside a frame made up of four template files:
By default, these are loaded from app/frontend/frame/. Every page on the site shares the same layout unless a module overrides it.
| Frame File | Purpose |
|---|---|
head.php |
Opening <html>, <head> with stylesheets, scripts, components, and <body> tag |
header.php |
Site header and navigation |
sideNav.php |
Optional side navigation panel |
footer.php |
Site footer, closing </body> and </html> tags |
Frame Overrides
A module can provide its own frame files by creating a frame/ directory. The framework checks the module's frame directory first, then falls back to the global frame:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $this->title ?? 'My Module' ?></title>
<!-- Custom head content here -->
<link rel="stylesheet" href="/css/custom-theme.css">
<!-- IMPORTANT: Call these to preserve auto-loading -->
<?php $this->getStylesheets(); ?>
<?php $this->getScripts(); ?>
<?php $this->getComponents(); ?>
</head>
<body>
If you override head.php, you must include $this->getStylesheets(), $this->getScripts(), and $this->getComponents(). Without these calls, the automatic CSS/JS/component loading for your module and its endpoints will not work.
Asset Loading Methods
| Method | Output |
|---|---|
$this->getStylesheets() |
<link> tags for matching CSS files in assets/css/ |
$this->getScripts() |
<script> tags for matching JS files in assets/js/ |
$this->getComponents() |
ShadowComponent HTML files from global and module component directories |
AJAX Requests
When a request is made with XMLHttpRequest (detected via X-Requested-With header), the frame is automatically skipped. Only the view content is returned, which makes it ideal for loading partial HTML into existing pages.
// Returns only the view HTML, no frame wrapper
const response = await fetch('/article/show/42', {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const html = await response.text();
document.getElementById('content').innerHTML = html;
View-Only Endpoints
If a module has no method for a requested endpoint but has a matching view file, the framework renders it automatically. This is useful for static content pages within a module:
Visiting /docs/about renders view/about.php without needing an about() method on the controller. The view still has access to $this and the full frame wrapping.