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.

app/Controllers/HomeController.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.

resources/views/home.php
layout('layouts.app', ['title' => $title]); ?>

<h1><?= e($framework) ?></h1>
<p>Your framework is running. Welcome!</p>

The template receives its own data plus $content:

resources/views/layouts/app.php (excerpt)
<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:

php
<?= 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.

php
<?= field('email', 'Email address', ['type' => 'email', 'autocomplete' => 'email']) ?>

rather than:

php
<?= 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):

php
<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'):

php
Lang::setLocale('en');

__('validation.required', ['attribute' => 'email']);  // "The email field is required."
__('http.404');                                        // "Page not found."
__('pagination.next');
FileContains
validation.phpOne message per rule, and the attributes section: readable field labels ('items.*.name' => 'item name').
upload.phpThe reasons for an invalid upload (size exceeded, partial upload...).
http.phpMessages of HTTP errors, authentication middleware and error pages.
pagination.phpThe “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

php
// 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):

HelperPurpose
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.
example: json_for_html() to hydrate a client component
<div data-props="<?= json_for_html($props) ?>" x-data="JSON.parse($el.dataset.props)"></div>
⏱ 3.38 ms 🗄 0 requête(s) SQL 🧠 4.00 MB ↩ 200