Views
Views
Ordinary .php files in resources/views/, with no separate template
engine — this documentation page is itself an example.
Native PHP views
Niang\Core\View does nothing exotic: renderView() extracts the data array
into the local scope (extract($data, EXTR_SKIP)), includes the file with
ob_start()/ob_get_clean(), and returns the captured HTML. No compilation,
no template bytecode cache: it is PHP, run as PHP.
public function index(): Response
{
return $this->view('home', ['title' => 'Welcome']);
}
\$this->view() (in a controller, through Niang\Core\Controller) and the
global view() helper both do the same thing: View::make($view, $data),
which wraps renderView() in a Response::html(). The path is resolved by
convention: 'home' → resources/views/home.php, and
'admin.posts.index' (dots as separators) →
resources/views/admin/posts/index.php. A missing view does not throw an exception: it
simply renders the text "Vue introuvable : name" (“View not found”) in place of the
content.
layout(): wrapping a page in a template
Called at the top of a view, layout($view, $data) renders nothing right away: it
remembers the target template. Once the view is fully rendered, its HTML is injected into the
template's $content, and the template is rendered in turn — a round trip handled by
View::renderView(), invisible from the view itself.
layout('layouts.app', ['title' => $title]); ?>
<h1><?= e($framework) ?></h1>
<p>Your framework is running. Welcome!</p>
The template receives its own data plus $content:
<title><?= e($title ?? 'NiangPro') ?></title>
...
<div class="card">
<?= $content ?>
</div>
This documentation site applies exactly that pattern: every view in
resources/views/docs/en/*.php (and docs/fr/*.php for French) starts with
layout('layouts.docs', [...]), and the template adds the sidebar, the contents and the
search around the content.
component(): including a partial view
component($view, $data) renders a view and returns the HTML directly as a string
(instead of a Response) — to insert it into another view:
<?= component('components/icon', ['name' => 'check']) ?>
Every icon in this documentation (the copy button, search, the mobile contents...) is a
component('components/icon', ['name' => '...']) — an inline SVG picked from
resources/views/components/icon-paths.php, never an icon font loaded from a remote
server.
field(): a complete form field
field($name, $label, $options = []) is a shortcut for
component('components/field', [...]): name and label are
required and come back in nearly every form view — repeating them as an associative array for each
field is the main visual noise of a form.
<?= field('email', 'Email address', ['type' => 'email', 'autocomplete' => 'email']) ?>
rather than:
<?= component('components/field', ['name' => 'email', 'label' => 'Email address', 'type' => 'email', 'autocomplete' => 'email']) ?>
$options accepts the same keys as the component (type — text by default,
rows for a <textarea>, value, autocomplete,
required). It is only a calling shortcut, not a new mechanism:
resources/views/components/field.php remains the only place that decides the HTML
produced — label, previous input through old() after a validation error (never for a
password, which is always emptied), and error message through
components/field-errors, already wired up.
e(): escaping output
e($value) is htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') — to
be used explicitly every time you output data that is not already trusted HTML (there is no
implicit automatic escaping, since there is no template engine):
<h1><?= e($title) ?></h1>
e() escapes for an HTML context. To inject data into an attribute consumed by
JavaScript (e.g. data-props for Alpine.js), use json_for_html()
instead — see the table below — which specifically escapes against breaking out of a
</script> tag or an attribute.
Languages: __() and lang/
The text a visitor sees — validation messages, upload errors, error pages, 401/403/419/429
messages, pagination — comes from lang/<locale>/*.php at the project root,
shipped in French (the default) and English. The locale is chosen in .env
(APP_LOCALE=en) or for one request with Lang::setLocale('en'):
Lang::setLocale('en');
__('validation.required', ['attribute' => 'email']); // "The email field is required."
__('http.404'); // "Page not found."
__('pagination.next');
| File | Contains |
|---|---|
validation.php | One message per rule, and the attributes section: readable field labels ('items.*.name' => 'item name'). |
upload.php | The reasons for an invalid upload (size exceeded, partial upload...). |
http.php | Messages of HTTP errors, authentication middleware and error pages. |
pagination.php | The “Previous” / “Next” links of Paginator::links(). |
These files belong to your project: edit a message directly, or add a language by copying
lang/en/ to lang/es/. :attribute, :min... are replaced,
:Attribute capitalizes the first letter. A key missing from the current locale is looked up
in APP_FALLBACK_LOCALE (fr), then shown as-is. The CLI and developer-facing
exceptions stay in French.
Plurals
// lang/en/cart.php
'articles' => 'one item|:count items',
'etat' => '{0} Your cart is empty|{1} One item|[2,9] :count items|[10,*] Over :count items',
trans_choice('cart.articles', 3); // "3 items"
Lang::choice('cart.etat', 0); // "Your cart is empty"
{n} and [min,max] forms are tried first, otherwise the language rule applies: in French,
0 and 1 are singular; in English, only 1 is.
Every global helper
Loaded automatically on every request (composer.json declares them under
autoload.files): no use statement to write, they are available
everywhere. The complete list (packages/core/src/helpers.php and packages/http/src/helpers.php):
| Helper | Purpose |
|---|---|
base_path(string $path = '') | Absolute path from the project root. |
env(string $key, mixed $default = null) | Reads a variable from .env. |
config(string $key, mixed $default = null) | Reads config/*.php, e.g. config('app.providers'). |
route(string $name, array $params = []) | URL of a named route. See Routing. |
signedRoute(string $name, array $params = [], ?int $expiresInSeconds = null) | route() + an expiring signature (UrlSignature::sign()) — a clickable link with no prior authentication. |
view(string $view, array $data = []) | Global equivalent of $this->view() in a controller. |
layout(string $view, array $data = []) | See above. |
component(string $view, array $data = []) | See above. |
field(string $name, string $label, array $options = []) | See above. |
e(mixed $value) | See above. |
__(string $key, array $replace = []) | Translation in the current locale, e.g. __('http.404'). See Languages. |
json_response(mixed $data, int $status = 200) | Global equivalent of Response::json(). |
csrf_token() | Current CSRF token (Niang\Core\Csrf::token()). |
csrf_field() | The CSRF <input type="hidden">, ready to insert into a <form>. |
old(string $key, mixed $default = null) | Previous input after a failed-validation redirect. See Validation. |
flashed(string $key, mixed $default = null) | Reads a session flash value (e.g. a success message after a redirect). |
errors(?string $key = null) | Flashed validation errors; with $key, those of a single field. |
abort(int $status, string $message = '') | Throws an HttpException, caught by Handler (e.g. abort(404)). |
json_for_html(mixed $data) | JSON escaped for an HTML attribute (JSON_HEX_*) — hydrate an Alpine.js/Vue/React component client-side with no XSS risk. |
uuid() | Random version 4 UUID, e.g. for a $table->uuid() column. |
url(string $path = '') | Absolute URL built from APP_URL (never the Host header): for links sent by email. |
trans_choice(string $key, int|float $count, array $replace = []) | Plural translation (see Plurals). |
trigger_deprecation(string $package, string $version, string $message) | Flags a deprecated API; logged in production (see API stability). |
vite_asset(string $entry) | Real URL of a Vite asset (Niang\Core\ViteAssets): switches to public/hot in dev, reads public/build/manifest.json in production. |
dd(mixed ...$vars) | Dumps each value (var_dump) and stops execution — debugging only. |
<div data-props="<?= json_for_html($props) ?>" x-data="JSON.parse($el.dataset.props)"></div>