phpaml / view
Declarative frontend interfaces for PHPAML, authored in PHP.
Requires
- php: ^8.2
- phpaml/engine: ^0.1.0-beta.9
Requires (Dev)
None
Suggests
- ext-bcmath: Exact decimal calculations in PHP rendering, reactive text and frontend events.
- ext-intl: Grapheme-aware text, Unicode normalization and localized number formatting in rendering and frontend events.
- ext-mbstring: Unicode mb_* functions in PHP rendering, reactive text and frontend events.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-16 19:27:14 UTC
README
Declarative frontend interfaces for PHPAML — authored entirely in PHP.
See the seven language extensions and their exact scope.
Status:
0.1.0-beta.13. The public API is ready for real-world evaluation, but backward compatibility is not guaranteed until1.0.0.
Documentation française · API reference · Security · Changelog
Frontend error handling: level 10 guide and service exception example.
Why AML View?
AML View adds an optional declarative UI layer without replacing classic PHP views. PHP renders the initial semantic HTML, then PHPAML Engine owns ordinary state, events and navigation directly in the browser.
- PHP 8.2+ with typed
#[State]properties; - cached
#[Computed]methods; - components, pages, layouts,
RouterView()andSlot(); - wrapper-free composition with
Group(); - frontend-only events by default, with explicit same-origin API actions;
- bound forms and declarative validation;
- no JSX, Node.js runtime or client framework required.
Install
Install the public beta directly from Packagist:
composer require phpaml/view:^0.1@beta phpaml/engine:^0.1@beta
Your first interactive page
<?php use AML\View\{Page, State, View}; use function AML\View\{Button, Heading, Text, VStack}; final class CounterPage extends Page { #[State] public int $count = 0; public function body(): View { return VStack( Heading('AML View')->fontSize(42)->fontWeight('bold'), Text($this->count), Button('Remove one') ->sets('count', $this->state('count')->minus(1)), Button('Add one') ->sets('count', $this->state('count')->plus(1)), )->gap(16)->padding(40); } }
The common path intentionally needs neither StateRef nor ClientAction.
Passing a #[State] property to a native text component keeps it reactive. Passing it
directly to a form control's value() creates a two-way binding:
Text($this->name); Input()->value($this->name);
Ordinary unmarked PHP properties remain static. The explicit
$this->state('property') API remains available for expressions. Safe
plus() and minus() expressions let sets() derive a new value from the
current state without JavaScript or executable serialized callbacks. The
existing increments(), decrements() and toggles() shortcuts remain
available for compatibility.
The lower-level Engine APIs remain available for sequences, transactions and
other advanced behavior.
Reactive state can also be interpolated into ordinary text without splitting a sentence into several view nodes:
Text('Count: {count}'); Text('Hello {profile.name}');
Placeholders must resolve to a property marked with #[State]. Nested array
paths are supported and remain HTML-escaped. Use doubled braces when text must
contain a literal placeholder, for example Text('Example: {{count}}').
The same text pipeline handles native headings, paragraphs, emphasis, labels, buttons, links, overlays and option labels:
Paragraph('Hello ' . $this->name); Heading("Hello {$this->name}", 2); Strong('{name}, and again {name}'); Text(number_format($this->price * $this->quantity, 2)); Text(fn () => $prefix . $this->name);
Display functions are read-only and use the supported frontend grammar. See the reactive text guide for the full component list, local captures, literal braces, diagnostics and compiler boundaries.
Simple frontend mutations can be written as ordinary PHP closures:
Button('Add')->onClick(function (): void { $this->count++; }); Button('Reset')->onClick(function (): void { $this->count = 0; });
PHPAML inspects these closures without executing them and compiles the supported
statements into the same declarative Engine actions shown above. The initial
subset supports ++, --, assignment, +=, -=, local variables and the
arithmetic operators +, -, *, / and % on #[State] properties:
Button('Calculate')->onClick(function (): void { $subtotal = $this->price * $this->quantity; $this->total = $subtotal - $this->discount; });
Normal PHP precedence and parentheses are preserved. Arbitrary PHP calls and unmarked properties are rejected.
Boolean negation, string concatenation and compound arithmetic are also supported,
for example $this->open = !$this->open, $this->label = 'Total: ' . $this->total
and $this->quantity *= 2.
PHP ternaries remain reactive as well:
$this->discount = $subtotal >= 100 ? 10 : 0; $this->label = $this->enabled ? 'Active' : 'Inactive';
Longer or reusable interactions may be moved into a named component method and passed as a first-class PHP callable:
private function increment(): void { $this->count++; } // Inside body() Button('Add')->onClick($this->increment(...));
The named method is inspected under the same frontend safety rules and is not executed during server rendering.
A single expression can use PHP's concise arrow-function syntax:
Button('Add')->onClick(fn () => $this->count++); Button('Toggle')->onClick(fn () => $this->enabled = !$this->enabled);
Use a braced closure or a named method when the interaction needs multiple statements.
Reactive output remains ordinary PHP. Use a validated state reference when the browser must recalculate a displayed value:
Text('Count: {count}'); Text($this->state('connected')->choose('Welcome', 'Sign in required')); Text($this->state('price')->times($this->state('quantity'))); Text($this->state('count')->prefixedBy('Count: ')); Text($this->state('count')->concat(' items'));
choose() and safe calculations update their text automatically when their
state dependencies change.
Basic collection changes use familiar PHP syntax inside safe frontend handlers:
$this->tasks[] = $this->newTask; array_pop($this->tasks); array_shift($this->tasks);
Collection operations use the closed native catalogue. Loaded user functions and service methods can also be compiled; see the compatibility matrix. Keep each frontend closure on its own lines so its source is unambiguous.
Conditions use ordinary PHP syntax and compile to safe Engine branches:
Button('Evaluate')->onClick(function (): void { if ($this->count >= 10) { $this->message = 'Goal reached'; } else { $this->message = 'Keep going'; } });
The initial condition grammar supports boolean states and comparisons with
scalar literals, state values or calculated local expressions: ==, ===,
!=, !==, >, >=, < and <=.
Frontend variables and scalar types
Frontend handlers support invocation-local variables, PHP scalar casts,
gettype(), browser-console var_dump(), file and component constants, and
single/double-quoted strings with interpolation and PHP escapes. Local
assignments snapshot their values instead of retaining live state references.
Use View beta.13 together with Engine beta.9. CLI beta.45 selects these
compatible minimum versions when creating a View application. See the
Level 1 guide (French) for examples,
precise limits, and the native-PHP/runtime parity check.
Frontend operators
Compiled handlers and effects support arithmetic, power, PHP strict/loose comparisons, logical short circuits, ternaries, null coalescing and pre/post increments. See the French level 2 guide for precedence, assignment expressions, examples and runtime limits.
Frontend conditions, loops and arrays
Handlers support switch, strict match, empty() and multi-argument isset().
for, while, do...while and foreach execute against current event state,
with nested loop controls and runtime iteration limits. PHP arrays preserve keys,
insertion order, numeric kinds and value-copy behavior across local assignments,
callbacks, destructuring and spread expressions. echo writes only to the browser
console. See the levels 3–5 guide (French)
for examples, diagnostics and the supported subset.
Functions and services
Frontend events can call user functions loaded from separate PHP service files, component methods and loaded static methods. Calls have private frames, real returns, typed/default/named/variadic parameters, references and closures. Recursion is bounded; synchronous failures restore component state. Function bodies are compiled without executing application code. See the level 6 guide (French) for examples and limits.
Selected PHP string and numeric builtins also compile into events, including trimming, validation, byte lengths, replacement, splitting/joining, rounding, secure random integers and amount formatting. Terminal streams, command arguments and process exits remain PHP CLI features; browser input uses controls and state. See levels 7–8 (French) and the input/menu example, which calculates prices in cents.
Bound and validated forms
final class ContactPage extends Page { #[State] public string $email = ''; public function body(): View { return Form( EmailInput() ->name('email') ->value($this->state('email')) ->required('Your email is required.') ->email(), Button('Send')->onClick( Api::post('/api/contact', ['email' => StateRef::to('email')]) ->loadingIn('sending') ->errorIn('contactError') ), ); } }
name() defines the submitted HTML field name. Passing a state reference to
value() creates the two-way reactive binding; passing a scalar instead keeps
the field non-reactive. model() remains available as the lower-level explicit
binding API.
PHPAML Engine validates controls locally and again before form submission. Invalid controls receive accessible error markup automatically.
Submit and field events use the same restricted frontend PHP closures as
onClick():
Form( TextField() ->name('name') ->value($this->state('name')) ->onInput(fn () => $this->editing = true) ->onBlur(fn () => $this->editing = false), Button('Send')->submit(), )->onSubmit(function (): void { $this->submitted = true; });
Available field events are onInput(), onChange(), onFocus() and
onBlur(). A frontend onSubmit() prevents native navigation after successful
local and remote validation; forms without it retain their normal HTML submit.
Status messages use the singular Alert API:
Alert::success('Saved'); Alert::error('Unable to save'); Alert::warning('Review this value'); Alert::info('A new version is available'); Alert::loading('Loading…');
Native progress can follow client state without a request or custom script:
use AML\Engine\StateRef; use function AML\View\Progress; Progress(StateRef::to('completion', $this->completion), 100) ->attribute('aria-label', 'Completion');
Changing completion through a declarative action updates both the state and
the browser's native progress value.
The original Alert($message, ActionStatus $status) function remains available
for compatibility.
Styling elements
Attach stylesheet classes declaratively:
Section( Heading('Welcome')->class('home-title'), )->class('home-hero', 'featured');
Keep global selectors in src/views/stylesheets/base.css. Stylesheets for
pages, components, layouts, and states should use class-based selectors.
File-based application
Projects created with aml create-view-app use a source-first structure:
src/
├── views/
│ ├── pages/
│ │ ├── home/page.php
│ │ ├── about/page.php
│ │ └── users/[id]/page.php
│ ├── components/
│ │ └── Navigation.php
│ ├── layouts/
│ │ └── AppLayout.php
│ ├── states/
│ │ ├── Loading.php
│ │ ├── Error.php
│ │ └── NotFound.php
│ ├── stylesheets/
│ ├── base.css
│ ├── pages/home.css
│ ├── components/navigation.css
│ ├── layouts/app.css
│ └── states/route-states.css
│ └── themes/
│ ├── light/tokens.css
│ └── dark/tokens.css
├── controllers/
├── models/
├── middleware/ (optional)
└── services/ (optional)
public/
├── index.php
├── favicon.svg
├── phpaml-logo-violet-lime.png
└── assets/
Folders under src/views/pages become URLs automatically:
src/views/pages/home/page.php handles /, while
src/views/pages/about/page.php handles /about. Layouts and route states are
kept in their own explicit directories. public/index.php remains the only
HTML document shell.
Dynamic folders use brackets. For example, src/views/pages/users/[id]/page.php
matches /users/42, and the page reads the value with $this->param('id').
states/Loading.php, states/Error.php, and states/NotFound.php provide declarative route
states. Components stay on the view side. Server-only controllers, models,
middleware, services, and API code live directly in src/. Classic PHP templates
are intentionally absent from AML View applications.
src/views, src/models, and src/controllers are required in every AML
View application. Middleware and service directories are optional.
Themes are native to AML View:
return ThemeProvider( default: 'system', content: Group(Navigation(), Slot()), themes: ['light', 'dark'], );
Place ThemeSwitcher('light', 'dark', 'system') in any component. AML View
applies the selected theme to <html>, observes the system preference, and
persists the choice without user-authored JavaScript. Theme CSS lives under
src/views/themes/{theme} and is bundled automatically.
SEO metadata is declarative and scoped to each page:
public function metadata(): PageMetadata { return (new PageMetadata()) ->title('User ' . $this->param('id')) ->description('Public user profile') ->canonical('https://example.com/users/' . $this->param('id')) ->language('en')->direction('ltr') ->favicon('/favicon.svg', 'image/svg+xml') ->meta('theme-color', '#ffffff') ->openGraph(image: 'https://example.com/assets/profile.png') ->twitter(image: 'https://example.com/assets/profile.png'); }
Call ->noIndex() for a page that search engines must not index.
FileApplication::head() safely renders the title, description, canonical,
robots, Open Graph, Twitter, favicon, preload and structured-data tags for the
active route. The generated document also receives the declared language and
text direction.
AML View contains no automatic server-interaction transport. Applications use
client actions for local behavior and an explicit Api::*() action when the
backend must be contacted.
Client-side engine
phpaml/engine executes ordinary interface state in the browser. A local
action does not call PHP or the AML interaction endpoint:
#[State] public int $count = 0; return VStack( Text(StateRef::to('count', $this->count)), Button('Add one')->onClick(ClientAction::increment('count')), );
The initial HTML and state still come from PHP. After mounting, PHPAML Engine
updates every element bound to count locally. Server communication remains an
explicit concern for future API actions, not the default behavior of a click.
State can be shared across routed pages or persisted by the browser:
#[State, Shared('cart.count'), Persisted('local', 'shop.cart.count', version: 2, expiresAfter: 86400)] public int $cartCount = 0; #[State, Persisted('session')] public int $checkoutStep = 1;
#[Shared] keeps every mounted property using the same key synchronized and
survives client-side navigation. #[Persisted] restores JSON-safe values from
localStorage or sessionStorage. Both may be combined on one property.
Every reusable component instance receives an isolated state namespace.
Increment version when a persisted schema changes. Incompatible data emits
aml:storage-migration-required; expired data emits aml:storage-expired and
is removed. AMLEngine.clearPersisted(key) explicitly resets a local value.
Local-storage changes are synchronized across browser tabs. Never persist
passwords, access tokens or other secrets.
Form state is also local:
Input('name', value: $this->name)->bindClient('name'); Text(StateRef::to('name', $this->name));
Typing updates the bound text immediately without registering a server event.
Validation is also frontend-native and remains declarative:
Input('email') ->bindClient('email') ->required('Email is required.') ->email() ->minLength(6);
PHPAML Engine validates during input/change and before form submission. It
sets aria-invalid, connects an accessible error with aria-describedby, and
focuses the first invalid control. API routes must still validate all received
data independently.
For availability or uniqueness checks, declare the API explicitly:
Input('name') ->bindClient('name') ->validateWith( Api::get('/api/validate-name', ['name' => StateRef::to('name')]), debounce: 300, );
The endpoint returns {"valid": true} or
{"valid": false, "message": "Already reserved."}. The engine debounces
input, cancels stale requests, restricts validation to same-origin URLs and
rechecks remote rules before submission.
Same-origin links use PHPAML Engine navigation automatically. The engine keeps
the document runtime mounted, updates history.pushState, supports browser
back/forward and marks the matching link with aria-current="page". Use
->nativeNavigation() when a link must perform a traditional document load.
Backend calls must be declared explicitly:
Button('Save')->onClick( Api::post('/api/profile', ['name' => StateRef::to('name')]) ->storeIn('profile') ->errorIn('apiError') ->loadingIn('apiLoading') );
API actions use same-origin requests with JSON payloads and browser credentials.
The response, error and loading states remain available through StateRef.
Use ->component('profile-card') to expose a named frontend lifecycle
boundary. PHPAML Engine emits mount, update and unmount events for the root and
these boundaries, cleans registered handlers and aborts pending API requests
when a page is removed.
Actions can be composed without JavaScript:
Button('Update')->onClick(Actions::sequence( ClientAction::increment('count'), Actions::when( 'count', 'gte', 10, ClientAction::set('message', 'Goal reached'), ClientAction::set('message', 'Keep going'), ), ));
Sequences preserve their order and can include explicit API actions.
Purely local sequences are automatically batched into one render. Use
Actions::transaction() when rollback semantics must also be explicit.
Presentation can depend directly on frontend state:
Button('Toggle') ->onClick(ClientAction::toggle('open')) ->classWhen(StateRef::to('open', $this->open), 'is-active'); Panel(...)->showWhen(StateRef::to('open', $this->open)); Button('Save')->disabledWhen(StateRef::to('loading', $this->loading)); When( StateRef::to('ready', $this->ready), Text('Ready'), Text('Waiting'), );
The initial HTML already reflects a supplied StateRef value, preventing a
visible flash before PHPAML Engine mounts.
The first reactive collection API uses Each() (foreach is reserved by PHP):
Each(StateRef::to('tasks', $this->tasks), label: 'title', key: 'id'); Button('Add')->onClick( ClientAction::append('tasks', [ 'id' => StateRef::to('newId'), 'title' => StateRef::to('newTitle'), ]) );
prepend(), removeAt(), removeBy(), updateBy(), filterBy(), sortBy(),
move(), reverse(), merge() and clear() are also available. Labels and keys may use nested paths such as
profile.name. Initial items are server rendered; subsequent collection
updates run locally and escape labels.
Nodes are reconciled by stable key. Reordering therefore preserves native DOM
state and AML component state. Components cloned for newly appended items
receive a key-derived isolated state namespace.
#[Computed] without dependencies remains a PHP-only value cached for one
server render. With explicit dependencies, AML View emits a safe frontend
calculation using concat, sum, count, all or any:
#[Computed(dependencies: ['first', 'last'], operation: 'concat', separator: ' ')] protected function fullName(): string { return $this->first . ' ' . $this->last; }
Declarative effects
#[Effect] connects browser-side work to state dependencies without sending a
click or a state update back to PHP. A void method can use the safe natural
frontend PHP subset and is compiled without being executed on the server:
#[Effect('search', runOnMount: false, debounce: 250)] private function searchChanged(): void { if ($this->search === '') { $this->message = 'Enter a search term'; } else { $this->message = 'Searching for ' . $this->search; } }
Multiple dependencies use #[Effect(['first', 'last'])]. Advanced effects may
still return a declarative AML instruction or an EffectPlan; PHP source is
never translated into arbitrary JavaScript.
For a short effect local to body(), use OnChange() and keep the named
attribute form for larger or reusable behavior:
return VStack( OnChange($this->state('age'), function (): void { $this->category = $this->age < 18 ? 'minor' : 'adult'; }), NumberInput()->name('age')->value($this->state('age')), Text('{category}'), );
OnChange() does not run initially by default. Pass initial: true when the
inline effect must also run on mount. Multiple dependencies are passed as an
array of state references.
For a React-style hook before the return statement, import the function and
register it while building body():
use AML\View\Effect; use function AML\View\Effect; public function body(): View { Effect(function (): void { $this->message = 'Search: ' . $this->search; }, [$this->state('search')], runOnMount: false); return VStack( TextField()->name('search')->value($this->state('search')), Text('{message}'), ); }
PHP keeps class and function imports in separate symbol tables, so the
#[Effect] attribute and Effect() function can coexist in the same file.
As with React Hooks, keep hook-style calls unconditional and before return.
The safe closure subset also accepts the browser-console facade directly:
use AML\View\Console; Effect(function (): void { Console::log('New counter value', $this->counter); }, [$this->state('counter')], runOnMount: false);
Console::log(), Console::warning() and Console::error() write to the browser
console. Loaded service static methods can also be compiled when their bodies
use the supported frontend language.
Action status without manual state
ActionState() wraps a frontend closure or client instruction and supplies
reactive pending, success, and error references automatically:
$save = ActionState(Api::post('/api/profile', [ 'name' => $this->state('name'), ])); return Form( TextField()->value($this->state('name')), Button($save->pending->choose('Saving...', 'Save')) ->onClick($save) ->disabledWhen($save->pending), Text($save->error), );
Each execution clears the previous error and success state, marks the action as
pending, then records success or the client error message. No extra #[State]
properties are required.
Complete form state
FormState() tracks the lifecycle of one native form without manual status
properties:
$form = FormState(); return Form( EmailInput() ->value($this->state('email')) ->required(), Button($form->submitting->choose('Sending...', 'Send')) ->submit() ->disabledWhen($form->invalid), Button('Reset')->onClick($form->reset()), Text($form->error), ) ->state($form) ->onSubmit($form->action( Api::post('/api/contact', ['email' => $this->state('email')]), ));
The handle exposes dirty, touched, valid, invalid, submitting,
submitted, and error. Native and remote validation remain authoritative.
Resetting the form also restores its bound reactive values.
Reducers, context and element references
Use Reducer() when one value has several named transitions:
$counter = Reducer(0, [ 'increment' => fn (StateRef $state) => $state->plus(1), 'decrement' => fn (StateRef $state) => $state->minus(1), ]); return VStack( Text($counter->state), Button('+')->onClick($counter->dispatch('increment')), Button('-')->onClick($counter->dispatch('decrement')), );
Context() and UseContext() are concise aliases for the existing provider
and reader contract:
Context('locale', 'fr-CA', UseContext('locale'));
Ref() provides an intentionally small imperative escape hatch. Supported
operations are focus, blur, select, play, and pause:
$search = Ref(); return Group( Input()->ref($search), Button('Focus')->onClick($search->focus()), );
Refs never execute arbitrary method names or JavaScript.
use AML\Engine\ClientAction; use AML\Engine\Effects; use AML\Engine\StateRef; use AML\View\Effect; #[Effect(dependencies: ['search'], runOnMount: false, debounce: 250, throttle: 500, concurrency: 'latest')] protected function synchronizeSearch(): \AML\Engine\EffectPlan { return Effects::run(ClientAction::set('query', StateRef::to('search'))); } #[Effect] protected function clock(): \AML\Engine\EffectPlan { return Effects::interval(1000, ClientAction::increment('seconds')); } #[Effect] protected function selection(): \AML\Engine\EffectPlan { return Effects::onDocument( 'app:selection', ClientAction::set('selectedId', \AML\Engine\EventRef::to('detail.id')), )->withCleanup(ClientAction::set('selectedId', null)); }
Available plans are Effects::run(), timeout(), interval(), onWindow()
and onDocument(). AML Engine cleans timers, listeners and active API requests
before an effect runs again and when its component is unmounted. It emits
aml:effect-run, aml:effect-cleanup, aml:effect-error and
aml:effect-cycle for diagnostics.
Concurrency strategies are latest, exhaust, queue, and parallel.
Dynamic collection components own isolated effects, stale async results are
discarded, and mount effects wait for IndexedDB restoration.
Development tools can inspect and control an effect without exposing mutable
runtime internals: AMLEngine.effects(root), pauseEffect(), resumeEffect()
and runEffect().
Rich components and collections
AML View now provides declarative, accessible building blocks without requiring application JavaScript:
return VStack( Button('Open profile')->onClick(ClientAction::set('profileOpen', true)), Modal(StateRef::to('profileOpen'), 'Profile', new ProfileCard()), Tabs(StateRef::to('section'), [ 'Overview' => new OverviewPanel(), 'Activity' => new ActivityPanel(), ]), Accordion(StateRef::to('expanded'), [ 'details' => Text('Account details'), ]), );
DataTable() sorts rows locally, VirtualList() renders only the visible
window of a large collection, SortableEach() supports local drag and drop,
and DynamicForm() keeps generated fields keyed and reactive.
AsyncBoundary() selects loading, success, empty, or error content from one
state value. Modal focus, tab arrows, accordion state, sortable table headers,
ARIA attributes, and transitions are handled by PHPAML Engine.
Virtual lists include an initial server-rendered window, then react to scrolling
and resizing in the browser. Sortable collections support pointer drag and drop
as well as Alt+ArrowUp and Alt+ArrowDown; moves never cross collection state
boundaries. Transitions automatically honor reduced-motion preferences.
PHP closure bodies compile into Engine instructions. Explicit eval() uses the same instruction set, with an optional PHP parser for dynamic strings; it never invokes JavaScript eval.
Several related mutations can be applied atomically with one render:
Button('Save')->onClick(Actions::transaction( ClientAction::set('profile.name', StateRef::to('draft.name')), ClientAction::set('profile.saved', true), ClientAction::sortBy('tasks', 'priority'), ));
Styles mirror the view tree. FileApplication::styles() bundles every CSS file
under src/views/stylesheets in deterministic order. The application exposes
that bundle at /_aml/styles.css, keeping public/ limited to the entry point
and static assets. Application images and media belong in public/assets.
Documents that require a direct top-level URL—such as the logo, favicon,
robots.txt, and sitemap.xml—stay at the root of public.
Declare which stylesheet belongs to a page or component with one top-level
import() outside the class. Its path is relative to src/views/stylesheets:
use function AML\View\{import, MainContent, Text}; import('pages/home.css'); final class HomePage extends Page { public function body(): View { return MainContent(Text('Home'))->class('home-page'); } }
import() accepts local .css files only, rejects traversal and remote URLs,
and deduplicates repeated declarations. class() remains responsible for
applying a stylesheet class to an element. The application keeps one global,
deterministic bundle so navigation never flashes or reloads page styles.
$app = new FileApplication(__DIR__ . '/../src/views'); $result = $app->mount($_SERVER['REQUEST_URI'] ?? '/');
Explicit routes and layouts
$layout = static fn (): Layout => new DashboardLayout(); $router = (new Router()) ->get('/', static fn (): View => new HomePage(), $layout) ->get('/users/{id}', static fn (array $params): View => new UserPage($params['id']), $layout); return RouterView($router);
Inside the layout, Slot() renders the active page.
Context and advanced navigation
Contexts carry configuration through layouts and components without global PHP variables. Values may be static or backed by client state, and may opt into local persistence:
return ContextProvider('locale', StateRef::to('locale', 'en'), ThemeProvider('system', VStack( ContextText('locale'), ThemeSwitcher('light', 'dark', 'system'), Slot(), )), persist: true, );
The built-in theme and locale contexts update the document theme and
language. Page::query() and queries() expose URL query parameters.
Navigate('/account') is a client instruction, while Redirect('/login')
performs a declarative replacement navigation.
Wrap routed content with NavigationBoundary() to keep loading, error, and
not-found states inside the current layout. Successful navigation updates
history, active links, title, managed SEO metadata, scroll position, focus, and
a reduced-motion-aware page transition without a full browser reload. Rapid
navigation cancels stale requests. Route states are loaded lazily and mounted
as interactive AML View trees. Browser tools can inspect
AMLEngine.context(root, name) and AMLEngine.route().
Accessible UI and advanced forms
AML View includes declarative Toast(), Dropdown(), MenuItem(),
Tooltip(), and Popover() primitives. PHPAML Engine manages their focus,
Escape handling, outside-click dismissal, live regions, ARIA state, and menu
keyboard navigation.
Forms support FileInput() with UploadForm(), ConditionalField(), and
MultiStepForm(). Add ->preserve('contact.draft') to retain ordinary values
across navigation and server errors. File contents are never persisted in
browser storage. After confirmed success, call
AMLEngine.clearFormDraft('contact.draft'). Always validate file size, type,
and content again on the server.
Demo and tests
Frontend integration is also checked from installed Composer distributions in
Chromium, Firefox and WebKit, with both the complete and modular Engine runtimes.
For a matching sibling Engine checkout, run npm run test:browser:runtimes and
npm run test:packages from that checkout. The package runner creates candidate
release ZIPs, installs them in isolation, checks their assets and runs the complete
task, reservation and compatibility examples. It does not publish packages.
The frontend-packages CI job repeats this on View changes; the repository
variable PHPAML_ENGINE_TEST_REF selects the matching Engine revision (default
main). See Engine's README for prerequisites and local overrides.
Construction-free view trees
Application code does not need to expose object construction. Generic elements
use the Element() factory, and a custom component may declare a same-name
factory next to its class:
final class Navigation extends Component { public function body(): View { return Element('nav', Link('Tasks', '/tasks')); } } function Navigation(mixed ...$arguments): Navigation { return new Navigation(...$arguments); }
Pages and layouts can then use Navigation() directly. FileApplication
preloads component files before pages and layouts, while
aml make:view-component Navigation generates the factory automatically.
Component(Navigation::class, ...) remains the generic fallback, and ordinary
new Navigation() stays fully compatible.
Use the built-in test API for fast component and page tests:
ViewTest::render(new CounterPage()) ->assertSee('Counter') ->assertState('count', 0) ->click('Add') ->assertState('count', 1);
It can inspect components and state, fill controls, simulate local AML actions, verify navigation, and assert exceptions without a browser or DOM extension.
composer test
php -S 127.0.0.1:8080 -t examples/aml-tasks/public examples/aml-tasks/public/router.php
Then open http://127.0.0.1:8080. AML Tasks is the complete reference
application: dashboard, reactive task collection, dynamic task route,
preferences, persistent themes, route states, responsive styles, controllers,
models, metadata and client-side navigation.
License
AML View is released under the MIT License.
Native PHP functions
View and Engine share a closed native catalogue. Math, string, array and scalar helpers support browser events and pure reactive displays; object member introspection runs in events. See the function catalogue and browser limits and the working example.
The Unicode guide covers 28 UTF-8 mb_* functions and a
reactive example. PHP rendering requires ext-mbstring.
The BCMath guide covers 14 exact decimal functions, rounding and event-local scale. See the decimal total example.
The output guide covers print_r() and var_export()
in events and reactive text, with a working example.
The eval guide covers static and dynamic PHP text in frontend events, shared local scope, catchable errors, limits and the optional parser asset.
The service object guide describes frontend constructors, typed properties, inheritance and method calls, with a complete cart example. Service static properties have component-local storage across events, including stored objects and closures; publish scalar or array results through State. See the two-counter example.
The magic methods guide covers service hooks for properties, methods, invokable objects, strings and cloning. The introspection guide adds __debugInfo(). See the profile example.
Anonymous classes and intersection/DNF contracts are supported in frontend events and services. See the guide and profile example.
The object introspection guide covers native object helpers, compiled enum detection and __debugInfo() console output. See the working example.
The collection objects guide covers Countable, ArrayAccess, is_countable() and direct/indirect offset operations. See the shopping-cart example.
The iterator guide covers Iterator, IteratorAggregate, Traversable, iterable and is_iterable(), including bounded foreach traversal. See the working collection example.
The generator guide covers lazy yield/yield from, suspension inside expressions, send, throw, getReturn, component-local lifetime and explicit cleanup boundaries. See the catalogue example.
The compatibility matrix is the entry point for the
supported language and remaining boundaries. Its native function list is generated
from Engine with composer docs:compatibility and checked with composer docs:check.
The combined example calls a
service using JSON, regular expressions,
dates, SPL collections and enums together.
The reservation example combines form validation,
createFromFormat, calendar intervals and periods, named callback arguments and
JsonSerializable in a separate service. See the date guide
and validation and callable utilities for their exact limits.
The URL, Unicode and dynamic-class guide covers
URL/Base64 helpers, filter_var_array, grapheme-safe reactive text, normalization,
localized numbers and currencies, dynamic class targets and complete nullsafe
receiver chains. See the combined profile example.