Frontend Extension
The Frontend Extension serializes PHP variables to JSON for use in HTML attributes. It converts PHP variables to JSON with proper escaping for frontend frameworks.
Note: This extension is essentially a convenient wrapper around Latte's built-in
escapeHtml()
andescapeJs()
filters, providing framework-specific attribute generation and automatic data serialization.
Features
- Universal Data Serialization: Convert any PHP variable (entities, arrays, scalars) to JSON
- Framework Agnostic: Support for frameworks that read complex data from HTML attributes (Alpine.js, Stimulus, HTMX, etc.)
- Named Components: Support for multiple controllers/components with different data
- Configurable: Easy framework mapping configuration
Installation & Configuration
Enable the Extension
Add the frontend extension to your View class by calling $this->getEngine()->addExtension()
:
// In your View class (e.g., src/View/AppView.php)
use LatteView\Extension\Frontend\FrontendExtension;
class AppView extends LatteView
{
public function initialize(): void
{
parent::initialize();
// Load the frontend extension
$this->getEngine()->addExtension(new FrontendExtension($this));
}
}
Custom Framework Mappings
Configure custom framework mappings by passing options to the extension constructor using the {name}
placeholder:
// In your View class
use LatteView\Extension\Frontend\FrontendExtension;
class AppView extends LatteView
{
public function initialize(): void
{
parent::initialize();
// Load the frontend extension with custom mappings
$this->getEngine()->addExtension(new FrontendExtension($this, [
'alpine' => 'x-data', // Default
'stimulus' => 'data-{name}-value', // Default
'htmx' => 'hx-vals', // Default
'vue' => ':data', // Custom
'turbo' => 'data-{name}-stream', // Custom
'custom' => 'data-{name}-props' // Custom
]));
}
}
The {name}
placeholder will be replaced with the component name specified in templates.
Usage
Basic Data Attributes
Generic Data Attribute
{* Creates data-json attribute *}
<div n:data="$user" data-my-framework="true">
Framework reads from data-json
</div>
Compiles to:
<div data-json="{"name":"John Doe","email":"john@example.com"}" data-my-framework="true">
Framework reads from data-json
</div>
AlpineJS Integration
{* Creates x-data attribute *}
<div n:data-alpine="$user">
<h1 x-text="name"></h1>
<p x-text="email"></p>
</div>
Compiles to:
<div x-data="{"name":"John Doe","email":"john@example.com"}">
<h1 x-text="name"></h1>
<p x-text="email"></p>
</div>
{* Works with scalar values *}
<div n:data-alpine="'Hello World'">
<span x-text="data"></span>
</div>
Compiles to:
<div x-data="{"data":"Hello World"}">
<span x-text="data"></span>
</div>
{* Works with arrays and collections *}
<div n:data-alpine="$articles">
<template x-for="article in data">
<div x-text="article.title"></div>
</template>
</div>
Compiles to:
<div x-data="[{"title":"Article 1"},{"title":"Article 2"}]">
<template x-for="article in data">
<div x-text="article.title"></div>
</template>
</div>
Stimulus Integration
{* Single controller *}
<div data-controller="user-profile" n:data-stimulus:user-profile="$user">
<span data-user-profile-target="name"></span>
</div>
Compiles to:
<div data-controller="user-profile" data-user-profile-value="{"name":"John Doe","email":"john@example.com"}">
<span data-user-profile-target="name"></span>
</div>
{* Multiple controllers with different data *}
<div data-controller="user-profile form-validator"
n:data-stimulus:user-profile="$user"
n:data-stimulus:form-validator="$validationRules">
<form data-form-validator-target="form">
<input data-user-profile-target="nameField" />
</form>
</div>
Compiles to:
<div data-controller="user-profile form-validator"
data-user-profile-value="{"name":"John Doe","email":"john@example.com"}"
data-form-validator-value="{"required":["name","email"]}">
<form data-form-validator-target="form">
<input data-user-profile-target="nameField" />
</form>
</div>
HTMX Integration
{* Creates hx-vals attribute *}
<button n:data-htmx="$params" hx-post="/api/update" hx-target="#result">
Update Data
</button>
Compiles to:
<button hx-vals="{"id":123,"action":"update"}" hx-post="/api/update" hx-target="#result">
Update Data
</button>
{* Form with HTMX data *}
<form n:data-htmx="$formData" hx-post="/submit" hx-swap="outerHTML">
<input type="text" name="title" />
<button type="submit">Submit</button>
</form>
Compiles to:
<form hx-vals="{"author_id":42,"category":"news"}" hx-post="/submit" hx-swap="outerHTML">
<input type="text" name="title" />
<button type="submit">Submit</button>
</form>
JavaScript Function Calls
For more advanced scenarios, you can use JavaScript mode (with -js
suffix) to call JavaScript functions with serialized PHP data as parameters:
Alpine.js Function Calls
{* Creates x-data with JavaScript function call *}
<div n:data-alpine-js="dropdown($params)">
<button x-on:click="toggle()">Toggle Dropdown</button>
<div x-show="isOpen" class="dropdown-menu">
<span x-text="config.type"></span>
</div>
</div>
Compiles to:
<div x-data="dropdown({"type":"xml","count":5})">
<button x-on:click="toggle()">Toggle Dropdown</button>
<div x-show="isOpen" class="dropdown-menu">
<span x-text="config.type"></span>
</div>
</div>
{* Multiple parameters *}
<div n:data-alpine-js="modal($config, $user)">
<button x-on:click="open()">Open Modal</button>
<div x-show="visible" class="modal">
<h2 x-text="title"></h2>
<p x-text="user.name"></p>
</div>
</div>
Compiles to:
<div x-data="modal({"theme":"dark"},{"name":"John","role":"admin"})">
<button x-on:click="open()">Open Modal</button>
<div x-show="visible" class="modal">
<h2 x-text="title"></h2>
<p x-text="user.name"></p>
</div>
</div>
Stimulus Function Calls
{* Creates data-*-value with JavaScript function call *}
<div data-controller="profile-menu" n:data-stimulus-js:profile-menu="initProfile($user)">
<button data-profile-menu-target="trigger">Menu</button>
<div data-profile-menu-target="menu">
<span data-profile-menu-target="username"></span>
</div>
</div>
Compiles to:
<div data-controller="profile-menu" data-profile-menu-value="initProfile({"name":"John","role":"admin"})">
<button data-profile-menu-target="trigger">Menu</button>
<div data-profile-menu-target="menu">
<span data-profile-menu-target="username"></span>
</div>
</div>
HTMX Function Calls
{* Creates hx-vals with JavaScript function call *}
<button n:data-htmx-js="getFormData($params)" hx-post="/api/submit" hx-target="#result">
Submit Form
</button>
Compiles to:
<button hx-vals="getFormData({"type":"xml","count":5})" hx-post="/api/submit" hx-target="#result">
Submit Form
</button>
Generic Function Calls
{* Creates data-json with JavaScript function call *}
<div n:data-js="setupWidget($user)">
Component with JavaScript function call
</div>
Compiles to:
<div data-json="setupWidget({"name":"John","role":"admin"})">
Component with JavaScript function call
</div>
When to Use JavaScript Mode
Use JavaScript mode (-js
suffix) when:
- You need to call JavaScript functions with dynamic data
- Your frontend framework expects function calls rather than plain JSON
- You want to initialize complex components with custom setup functions
- You need to pass multiple separate data objects to a single function
Use regular mode (no suffix) when:
- You want to pass plain JSON data objects
- Your framework reads data directly from attributes
- You're using simple data binding scenarios
Data Types
The extension handles all PHP data types:
CakePHP Entities
{* Automatically uses toArray() method *}
<div n:data-alpine="$user">
<span x-text="name"></span>
<span x-text="email"></span>
</div>
Arrays and Collections
{* Direct array serialization *}
<div n:data-alpine="$articles">
<template x-for="article in data">
<div x-text="article.title"></div>
</template>
</div>
Scalar Values
{* Wrapped in {data: value} for consistency *}
<div n:data-alpine="'Hello World'">
<span x-text="data"></span>
</div>
<div n:data-alpine="42">
<span x-text="data"></span>
</div>
Mixed Data
{* Combine different data types *}
<div n:data-alpine="[
'message' => $message,
'count' => $count,
'users' => $users,
'config' => ['theme' => 'dark']
]">
<span x-text="message"></span>
<span x-text="count"></span>
<template x-for="user in users">
<div x-text="user.name"></div>
</template>
</div>
Framework-Specific Examples
AlpineJS
{* Component with reactive data - include all data from controller *}
<div n:data-alpine="['user' => $user, 'editing' => false]">
<template x-if="!editing">
<div>
<h2 x-text="user.name"></h2>
<button @click="editing = true">Edit</button>
</div>
</template>
<template x-if="editing">
<form @submit.prevent="editing = false">
<input x-model="user.name" />
<button type="submit">Save</button>
</form>
</template>
</div>
Stimulus
{* User profile controller *}
<div data-controller="user-profile" n:data-stimulus:user-profile="$user">
<img data-user-profile-target="avatar" />
<h1 data-user-profile-target="name"></h1>
<button data-action="click->user-profile#edit">Edit Profile</button>
</div>
{* Form validation controller *}
<form data-controller="form-validator"
n:data-stimulus:form-validator="$validationRules"
data-action="submit->form-validator#validate">
<input data-form-validator-target="field" name="email" />
<div data-form-validator-target="errors"></div>
</form>
HTMX
{* Dynamic content loading *}
<div hx-get="/users" n:data-htmx="['page' => 1, 'limit' => 10]">
Loading users...
</div>
{* Form submission with context *}
<form hx-post="/articles" n:data-htmx="['author_id' => $currentUser->id]">
<input name="title" placeholder="Article title" />
<textarea name="content" placeholder="Content"></textarea>
<button type="submit">Publish</button>
</form>
XSS Protection Example
{* Dangerous input *}
<div n:data-alpine="['script' => '<script>alert(\'xss\')</script>', 'safe' => 'normal text']">
<span x-text="safe"></span>
</div>
Safely compiles to:
<div x-data="{"script":"\\u003Cscript\\u003Ealert(\\u0027xss\\u0027)\\u003C\\/script\\u003E","safe":"normal text"}">
<span x-text="safe"></span>
</div>
Notice how:
<
becomes\\u003C
(escaped angle bracket)'
becomes\\u0027
(escaped quote)/
becomes\\/
(escaped slash)- The malicious script is completely neutralized
Helper Functions
The extension provides a convenient helper function for templates:
{* Convert any data to JSON *}
{json($user)}