This guide assumes that you have never created a PHPAML project. We will not jump from installation to a finished screenshot. Every command, folder, deletion and line of PHP has a purpose. At the end, you will have a small but complete AML View application: a counter whose value changes instantly in the browser when you press plus or minus, without requesting a full page render from the server.
The counter is deliberately simple. It lets us observe the entire PHPAML frontend cycle without hiding the important ideas behind a large application: PHP renders the initial document, AML View describes the interface, AML Engine activates the declared actions, and client state updates only the affected elements.
1. Know exactly what you are going to build
The final page contains a title, an explanation, the current value, a minus button, a plus button and a reset button. The value starts at zero. Plus adds one. Minus subtracts one but becomes disabled at zero, so the counter never becomes negative. Reset returns the value to zero and is also disabled when there is nothing to reset.
You will create the project, launch it, inspect its structure, remove the demonstration-specific content, write a minimal page, attach CSS classes, verify reactivity and understand what happens after each click. No database, API or custom JavaScript is needed for this first exercise.
2. Verify the self-contained PHPAML installation
When you use the official PHPAML installer, you normally do not need to install PHP or Composer separately. AML includes its own compatible PHP runtime and a private copy of Composer. The framework requires PHP 8.2 or newer, but the runtime shipped with the tool already satisfies that requirement.
Open a terminal and run these two commands:
aml version
aml doctorThe first command confirms that `aml` is available and prints its version. The second checks the PHP runtime AML actually uses, its available extensions, and the components required by the CLI. This guide was verified with AML 1.7.0-beta.22; a newer compatible release may naturally print a different version.
Do not use `php -v` as the primary check for this tutorial. That command inspects the machine-wide PHP installation, when one exists, while AML prefers its private runtime. The `php` command may therefore be absent from the terminal even though `aml create-view-app` and `aml serve` work correctly.
Some advanced projects may require a PHP extension that is not included in the bundled runtime. Only in that situation does AML automatically look for another compatible PHP executable installed on the machine. The diagnostic clearly reports any missing extension. This first AML View counter should not require a separate PHP installation.
If `aml version` is not found, install PHPAML from the official download page, close and reopen the terminal, and try again. If `aml doctor` reports an error, resolve that diagnostic before creating the project; continuing with an incomplete environment would make every later error harder to understand.
3. Choose a clean workspace
Move to a directory where a new folder may be created. The Desktop is convenient for learning. Check that a folder named first-counter does not already contain work you care about.
cd ~/DesktopThe creation command refuses unsafe overwrites. Still, good habits begin by knowing where the command will write. In a professional repository, create the project in your usual development directory instead.
4. Create the AML View application
Run the dedicated View command. Do not use the removed aml install view workflow.
aml create-view-app first-counter
cd first-counterAML creates the project, installs the compatible View and Engine runtimes, prepares autoloading and validates that the generated application can load FileApplication, EngineRuntime and the security components. If creation stops with an error, do not continue with a half-created application: read the reported missing dependency, correct it and run the command in a new empty folder.
5. Start the development server
Launch the project from its root.
aml serveAML first tries http://127.0.0.1:8910. If that port is occupied, it tries 8911, then the next available port. Use the exact address printed by the terminal. Keep this terminal open: stopping the process stops the development server. Open a second terminal for later commands.
Visit the address in your browser. The generated demonstration should appear without a fatal error. This first successful page proves that PHP, the project runtime and the public entry point can work together.
6. Understand the generated structure before cleaning it
The important folders are intentionally separated.
first-counter/
public/
index.php
src/
controllers/
models/
views/
pages/
home/
page.php
components/
layouts/
states/
stylesheets/
themes/
routes/
runtime/
phpaml.jsonpublic/index.php is the single web entry point and should remain small. src/views/pages contains file-based pages. The home/page.php file maps to /. Components and layouts hold reusable visual structures. stylesheets and themes belong to the interface. controllers and models remain available for server logic even though this counter does not need them yet. runtime contains framework code, installed modules and generated state; do not redesign the application by editing runtime internals. phpaml.json contains project-level configuration.
7. Decide what “clean the project” actually means
Cleaning does not mean deleting every unfamiliar folder. The generated home page demonstrates state, API calls, collections, themes and components; we only need to replace that demonstration with our focused exercise. Keep public/index.php, phpaml.json, runtime, src/controllers, src/models and src/views. Keep the existing layout and navigation if you want the standard shell.
For this exercise, replace the content of src/views/pages/home/page.php and replace the demonstration rules in src/views/stylesheets/pages/home.css. Do not leave old classes that are no longer rendered. Unused CSS makes later debugging confusing because nobody knows whether a selector is required. Do not delete global styles, theme tokens or route-state styles: they support the application shell and error pages.
8. First modification: open and replace the home page
The first file to edit is **`src/views/pages/home/page.php`**. It maps to the home route, **`/`**. Keep the terminal running `aml serve` open. In a second terminal, move to the project root and open exactly this file:
cd ~/Desktop/first-counter
code src/views/pages/home/page.phpIf `code` is unavailable, open the `first-counter` folder in VS Code, expand `src`, `views`, `pages`, and `home`, then click `page.php`. Do not create another file with the same name. Select everything currently inside `page.php`, delete it, and paste this complete first version:
<?php
declare(strict_types=1);
namespace App\Views\Pages\Home;
use AML\View\Page;
use AML\View\PageMetadata;
use AML\View\View;
use function AML\View\{Heading, MainContent, Section, Text, VStack};
final class HomePage extends Page
{
public function metadata(): PageMetadata
{
return new PageMetadata(
'My first PHPAML counter',
'A small reactive counter built with AML View.',
);
}
public function body(): View
{
return MainContent(
Section(
VStack(
Text('PHPAML · STEP BY STEP')->class('counter-eyebrow'),
Heading('My first reactive counter'),
Text('The value updates directly in the browser.'),
)->gap(16),
)->class('counter-page', 'shell'),
);
}
}Save exactly `src/views/pages/home/page.php` with `⌘S` on macOS or `Ctrl+S` on Windows and Linux. Return to the address printed by `aml serve`, such as `http://127.0.0.1:8910`, and refresh. You should see the new heading and sentence, with no number or button. This check proves that you are editing the correct file before introducing state.
The `App\Views\Pages\Home` namespace matches the folder path. `metadata()` describes the document and `body()` describes the interface. The file contains no raw HTML tags.
9. Second modification: add and display state
Stay in **`src/views/pages/home/page.php`**. No new file is needed. Add three things: the `StateRef` import, the `State` import, and the `$count` property in the class. Inside `body()`, the local `$count` variable connects that property to visible text.
To remove all uncertainty about placement, replace the complete contents of `src/views/pages/home/page.php` with this second version:
<?php
declare(strict_types=1);
namespace App\Views\Pages\Home;
use AML\Engine\StateRef;
use AML\View\Page;
use AML\View\PageMetadata;
use AML\View\State;
use AML\View\View;
use function AML\View\{Heading, MainContent, Section, Text, VStack};
final class HomePage extends Page
{
#[State]
public int $count = 0;
public function metadata(): PageMetadata
{
return new PageMetadata(
'My first PHPAML counter',
'A small reactive counter built with AML View.',
);
}
public function body(): View
{
$count = StateRef::to('count', $this->count);
return MainContent(
Section(
VStack(
Text('PHPAML · STEP BY STEP')->class('counter-eyebrow'),
Heading('My first reactive counter'),
Text('The value updates directly in the browser.'),
Text($count)->class('counter-value'),
)->gap(16),
)->class('counter-page', 'shell'),
);
}
}Save the same file and refresh the browser. The number `0` should appear below the sentence. No button exists yet, so it cannot change. `#[State]` declares typed reactive data; `StateRef::to('count', $this->count)` creates its client binding; `Text($count)` marks the exact place to update.
If the browser still shows only the heading, verify that `Text($count)` is inside `VStack`, immediately after the sentence. Do not use `Text((string) $this->count)`: that would be static text computed once by PHP.
10. Third modification: add the plus button
The file remains **`src/views/pages/home/page.php`**. Add `ClientAction` near the top, add `Button` to the function import, and place the `+` button immediately after `Text($count)`.
Replace the entire file with this complete third version:
<?php
declare(strict_types=1);
namespace App\Views\Pages\Home;
use AML\Engine\ClientAction;
use AML\Engine\StateRef;
use AML\View\Page;
use AML\View\PageMetadata;
use AML\View\State;
use AML\View\View;
use function AML\View\{Button, Heading, MainContent, Section, Text, VStack};
final class HomePage extends Page
{
#[State]
public int $count = 0;
public function metadata(): PageMetadata
{
return new PageMetadata(
'My first PHPAML counter',
'A small reactive counter built with AML View.',
);
}
public function body(): View
{
$count = StateRef::to('count', $this->count);
return MainContent(
Section(
VStack(
Text('PHPAML · STEP BY STEP')->class('counter-eyebrow'),
Heading('My first reactive counter'),
Text('The value updates directly in the browser.'),
Text($count)->class('counter-value'),
Button('+')
->onClick(ClientAction::increment('count'))
->attribute('aria-label', 'Increase the counter')
->class('counter-button', 'counter-button-plus'),
)->gap(16),
)->class('counter-page', 'shell'),
);
}
}Save `page.php`, refresh once, and click `+` several times. You should see `0 → 1 → 2 → 3` immediately, with no flash or full reload. PHP supplied the initial document; AML Engine then executes `ClientAction::increment('count')` in the browser.
If the button appears but does not react, check both imports in this file. `ClientAction` comes from `AML\Engine`; `Button` belongs in `use function AML\View\{...};`.
11. Fourth modification: add minus and protect zero
Continue in **`src/views/pages/home/page.php`**. Add `Element` to the function import. Replace the isolated plus button with a container holding minus and plus. Minus uses `decrement` and `disabledWhen`.
Replace all of `page.php` with this fourth version:
<?php
declare(strict_types=1);
namespace App\Views\Pages\Home;
use AML\Engine\ClientAction;
use AML\Engine\StateRef;
use AML\View\Page;
use AML\View\PageMetadata;
use AML\View\State;
use AML\View\View;
use function AML\View\{Button, Element, Heading, MainContent, Section, Text, VStack};
final class HomePage extends Page
{
#[State]
public int $count = 0;
public function metadata(): PageMetadata
{
return new PageMetadata(
'My first PHPAML counter',
'A small reactive counter built with AML View.',
);
}
public function body(): View
{
$count = StateRef::to('count', $this->count);
return MainContent(
Section(
VStack(
Text('PHPAML · STEP BY STEP')->class('counter-eyebrow'),
Heading('My first reactive counter'),
Text('The value updates directly in the browser.'),
Text($count)->class('counter-value'),
Element('div',
Button('−')
->onClick(ClientAction::decrement('count'))
->disabledWhen($count, 0)
->attribute('aria-label', 'Decrease the counter')
->class('counter-button', 'counter-button-minus'),
Button('+')
->onClick(ClientAction::increment('count'))
->attribute('aria-label', 'Increase the counter')
->class('counter-button', 'counter-button-plus'),
)->class('counter-controls'),
)->gap(18)->class('counter-card'),
)->class('counter-page', 'shell'),
);
}
}Save and refresh. At zero, minus must be disabled. Click plus twice: the value reaches 2 and minus becomes active. Click minus twice: the value returns to 0 and minus becomes disabled again. Perform this check now, before adding Reset.
The `Element('div', ...)` container only groups controls. `disabledWhen($count, 0)` observes the same state as the displayed number and prevents the user from going below zero.
12. Fifth modification: add Reset
Make the final PHP change in **`src/views/pages/home/page.php`**. Insert Reset between minus and plus. It directly sets the state to zero with `ClientAction::set`.
Replace `page.php` one final time with this complete version:
<?php
declare(strict_types=1);
namespace App\Views\Pages\Home;
use AML\Engine\ClientAction;
use AML\Engine\StateRef;
use AML\View\Page;
use AML\View\PageMetadata;
use AML\View\State;
use AML\View\View;
use function AML\View\{Button, Element, Heading, MainContent, Section, Text, VStack};
final class HomePage extends Page
{
#[State]
public int $count = 0;
public function metadata(): PageMetadata
{
return new PageMetadata(
'My first PHPAML counter',
'A small reactive counter built with AML View.',
);
}
public function body(): View
{
$count = StateRef::to('count', $this->count);
return MainContent(
Section(
VStack(
Text('PHPAML · STEP BY STEP')->class('counter-eyebrow'),
Heading('My first reactive counter'),
Text('The value updates directly in the browser.'),
Text($count)->class('counter-value'),
Element('div',
Button('−')
->onClick(ClientAction::decrement('count'))
->disabledWhen($count, 0)
->attribute('aria-label', 'Decrease the counter')
->class('counter-button', 'counter-button-minus'),
Button('Reset')
->onClick(ClientAction::set('count', 0))
->disabledWhen($count, 0)
->class('counter-reset'),
Button('+')
->onClick(ClientAction::increment('count'))
->attribute('aria-label', 'Increase the counter')
->class('counter-button', 'counter-button-plus'),
)->class('counter-controls'),
)->gap(18)->class('counter-card'),
)->class('counter-page', 'shell'),
);
}
}Save and refresh. Click plus five times, minus once, then Reset. The expected sequence is `0 → 5 → 4 → 0`. At zero, minus and Reset must be disabled. Every version shown since step 8 was complete, saveable, and testable, so the reader never has to guess where an isolated excerpt belongs.
13. Verify which files you changed
At this point, only one PHP file has changed: `src/views/pages/home/page.php`. You did not edit `public/index.php`, runtime files, controllers, or models. That boundary matters: the page describes the view, the entry point starts the application, and runtime belongs to the framework.
If the counter works but still looks plain, that is expected. PHP behavior is complete. The next step edits a second file only for presentation: **`src/views/stylesheets/pages/home.css`**.
14. Understand the result before moving to CSS
The property marked `#[State]` supplies the initial value. `StateRef` connects it to visible elements. The three `ClientAction` declarations describe decrease, reset, and increase. An ordinary click does not ask the server to rebuild the whole page.
You may now close `page.php` in the editor if you wish. Do not paste CSS into this PHP file. Open the stylesheet named in the next step.
15. Replace the page stylesheet
Open src/views/stylesheets/pages/home.css, remove the old demonstration rules and add only class selectors used by the new page.
.counter-page {
min-height: calc(100vh - 8rem);
display: grid;
place-items: center;
padding-block: 4rem;
}
.counter-card {
width: min(100%, 38rem);
padding: clamp(2rem, 6vw, 4.5rem);
align-items: center;
text-align: center;
border: 1px solid var(--line);
background: var(--panel);
box-shadow: var(--shadow);
}
.counter-eyebrow {
color: var(--lime);
font-size: .75rem;
font-weight: 850;
letter-spacing: .14em;
}
.counter-value {
min-width: 4ch;
color: var(--violet);
font: 800 clamp(4rem, 18vw, 8rem)/1 ui-monospace, monospace;
}
.counter-controls {
display: flex;
align-items: center;
justify-content: center;
gap: .75rem;
flex-wrap: wrap;
}
.counter-button,
.counter-reset {
min-width: 3.25rem;
min-height: 3.25rem;
border: 1px solid var(--line);
color: var(--ink);
background: var(--soft-fill);
font: inherit;
font-weight: 800;
cursor: pointer;
}
.counter-button {
font-size: 1.5rem;
}
.counter-button-plus {
border-color: var(--lime);
color: #17110d;
background: var(--lime);
}
.counter-button:disabled,
.counter-reset:disabled {
opacity: .4;
cursor: not-allowed;
}These selectors are scoped by classes attached in page.php. The stylesheet does not redefine every button, heading or div in the application. Existing light and dark theme tokens continue to supply colors through CSS variables. Because AML View discovers stylesheets recursively, there is no link tag to add to public/index.php.
16. Verify the complete interaction sequence
Do not stop after one successful click. Test a precise sequence.
- Reload the page and confirm that the value is 0.
- Confirm that minus and reset are disabled.
- Click plus once and confirm that the value becomes 1.
- Confirm that minus and reset become enabled.
- Click plus four more times and confirm that the value becomes 5.
- Click minus twice and confirm that the value becomes 3.
- Click reset and confirm that the value returns to 0.
- Confirm that minus and reset become disabled again.
- Use the keyboard Tab key and activate each enabled button with Enter or Space.
- Resize the page to a narrow mobile width and confirm that the controls remain readable.
Also watch for what should not happen: no full-page reload, no negative value, no duplicated number, no raw HTML tag displayed as text and no error in the browser console.
17. Understand why the state resets after a browser refresh
Click plus, then reload the browser. The value returns to zero. This is expected because #[State] creates reactive interface state, not permanent storage. The first document is rendered again by PHP and the initial property value is zero.
If you want the browser to remember the count locally, add Persisted('local', 'first-counter.value') beside State. If the value must belong to a user and survive on another device, expose a backend endpoint and persist it with PHPAML Data. Do not confuse browser persistence with trusted server data. For this first guide, leaving the reset behavior visible teaches the boundary clearly.
18. Diagnose the most common mistakes
If the page displays but buttons do nothing, confirm that onClick receives ClientAction and that the Engine runtime is present. If a class is not found, compare the namespace with src/views/pages/home. If the number stays at zero, make sure Text receives StateRef rather than a converted string. If styles do not appear, confirm that home.css is inside src/views/stylesheets and that the class names match exactly.
If aml serve reports an occupied port, use the new printed port instead of assuming 8910. If the browser shows an old version, perform a normal refresh and check that you edited the project currently served by the terminal. Never repair a generated application by copying random framework classes into public; recreate the project when its installed runtime is incomplete.
19. Make one controlled extension
Once the basic sequence works, try changing the step. ClientAction::increment accepts a second number.
ClientAction::increment('count', 5)
ClientAction::decrement('count', 5)If you choose a step of five, reconsider the zero rule: subtracting five from a value smaller than five could become negative. This shows why a small visual change can introduce a domain rule. Keep the original step of one until you have explicitly designed the new boundary.
Summary
You started from an empty location, verified the tools, created an AML View project, launched its development server, understood its folders and cleaned only the demonstration-specific files. You then created typed state, displayed it through StateRef and changed it with declarative ClientAction instructions. The minus and reset controls react to the same state and protect the zero boundary. CSS stays in src/views/stylesheets and targets explicit classes.
Most importantly, you observed the PHPAML execution model. PHP and AML View produce the first document. AML Engine activates safe instructions in the browser. An ordinary counter click changes local state and the bound text without asking the server to render the whole page. The next natural exercise is to persist meaningful data through an API and PHPAML Data, while keeping this same separation between interface, application logic and storage.