# PHPue AI Knowledge (full text) > The companion AI knowledge base for PHPue 0.0.3, with coding-agent rules, verified examples and downloadable skills. Official framework documentation: https://phpue.co.uk/docs. PHPue version: 0.0.3 | Updated: 2026-10-12 | Index: https://ai.phpue.co.uk/llms.txt --- # What PHPue is (and is not) > **PHPue** is a server-side PHP framework by Edward Patch that compiles Vue-like single-file components (`.pvue` files) into plain PHP. A `.pvue` file has a `
``` ```html
Home
``` Run it with `cd www && php -S localhost:3000` and open `http://localhost:3000`. ## Where to go next - Writing PHPue with an AI assistant or coding agent: [Rules for AI coding agents](/docs?topic=ai-rules). - First app step by step: [Quick start](/docs?topic=quickstart). - Syntax in depth: [.pvue files](/docs?topic=pvue-files), [Templates](/docs?topic=templates), [Components](/docs?topic=components), [Routing](/docs?topic=routing), [@AJAX](/docs?topic=ajax). - Something renders wrong: [Common mistakes](/docs?topic=gotchas). - Everything in one file for an LLM: [/llms-full.txt](/llms-full.txt). --- # Rules for AI coding agents > Read this before generating any PHPue code. PHPue is new and small, so general training data about it is thin and often wrong. When this page disagrees with older blog posts or docs (for example `{{ $user.name }}`, `p-model` or "PHP 7.1+"), **this page reflects what the PHPue 0.0.3 compiler actually does**. Every rule here was verified by compiling real `.pvue` files. ## Step 1: check the version ```bash grep -m1 PHPUE_VERSION www/index.php # define('PHPUE_VERSION', '0.0.3'); ``` These rules are for **0.0.3**. 0.0.2 is much stricter about markup layout; if the project is on 0.0.2, read [Upgrading from 0.0.2](/docs?topic=upgrade) first and either upgrade (replace `index.php` **and** `conversion.php` together) or follow the 0.0.2 rules there. Never write 0.0.3-only markup into a 0.0.2 project. ## The hard rules 1. **A `.pvue` file has up to four top-level blocks, in this order:** `
``` `#require` registers each view. `` is replaced by the page for the current URL. ## 3. Add a component: `www/components/Navbar.pvue` ```html ``` ## 4. Add pages: `www/views/index.pvue` and `www/views/about.pvue` ```html
Home
console.log('Features from PHP:', {{ $features }}); ``` ```html
About
``` ## 5. Run it ```bash cd www php -S localhost:3000 ``` Open `http://localhost:3000` (use `localhost`, not `127.0.0.1`). Add `?live` to the URL for hot reload while you edit. Source mode compiles every `.pvue` on each request, so edits show up on refresh. Docker alternative (nginx + php-fpm on port 3000): use the `docker-compose.yaml` and `nginx/default.conf` from the PHPue repository and run `docker compose up`. ## 6. Build and deploy ```bash cd www php index.php build # compiles everything into .dist/ ``` Upload `.dist/`, `assets/`, `httpReqs/`, `index.php`, `conversion.php` and `.htaccess` to any PHP 8 host. `index.php` serves the precompiled `.dist/` automatically. Delete `.dist/` locally when you go back to editing, otherwise the old build keeps being served. Details: [Build and deploy](/docs?topic=build-deploy). ## Next steps - Add server actions without page reloads: [@AJAX](/docs?topic=ajax). - Share data and classes between pages: [backend/ and scope](/docs?topic=backend). - Style with Tailwind: add `tailwindcss` as a dev dependency, set `content` to `["./**/*.pvue", "./**/*.php"]`, build to `assets/css/tailwind-styles.css` and link it in the App header. Rebuild CSS after adding classes. - Complete examples: [Recipes](/docs?topic=recipes). --- # .pvue files: the four blocks > A `.pvue` file is a single-file component with up to four top-level blocks: `
Example
// Browser JavaScript, printed as a ` (or ` ``` ```html ``` - `@require` takes single or double quotes; the path is relative to the web root (`www/`). - Tags: ``, `` or ``. **No other attributes and no children.** The only exceptions are `p-if` and `p-for` on the tag (0.0.3). - Components can `@require` other components. Paths are always from the web root. - A missing file compiles to a comment and renders nothing. Using `` without `@require` renders nothing useful either. ## Giving a component data (instead of props) Set variables right before the tag, in raw PHP: ```html ``` ```php // components/StatsPanel.pvue ``` Always sanitise or allow-list query values before using them in paths or queries. For pretty URLs, the author's PrettyUrlRedirect extension 301-redirects `/page/key/value` to `/page?key=value`. ## Navigation helpers Available in every `.pvue` file and in `backend/`: - `phpue_navigation()` returns `[['name' => 'about', 'title' => 'About', 'url' => '/about'], ...]`. The title comes from each view's `` (or `ucfirst(name)`). Order is file order, so sort or filter it yourself. - `get_current_route()` returns the current slug (`index` for `/`). - `phpue_current_meta()` returns the current view's parsed header meta. - Active link: compare `parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)` with the link URL and set `aria-current="{{ $isCurrent ? 'page' : 'false' }}"`. ```html <!-- components/Navbar.pvue --> <script> $navRoutes = array_filter(phpue_navigation(), fn($r) => $r['name'] !== 'admin'); $navCurrent = get_current_route(); </script> <template> <nav aria-label="Main"> <a p-for="$r in $navRoutes" href="{{ $r['url'] }}" aria-current="{{ $r['name'] === $navCurrent ? 'page' : 'false' }}">{{ $r['title'] }}</a> </nav> </template> ``` ## 404 pages Unknown routes return HTTP 404 and render inside the App layout (navbar and footer stay): - `httpReqs/http404.php`: plain PHP/HTML body that replaces `<View/>`. - `httpReqs/http404Head.php`: head tags for the 404 page: ```php <?php global $phpue_header; $phpue_header = <<<HTML <title>404 - Page Not Found HTML; $GLOBALS['phpue_http404_header'] = $phpue_header; ?> ``` Without these files PHPue prints a default "404 - Page Not Found" heading. Only 404 is built in; for other errors, set `http_response_code()` in the view script. ## Where the view's code runs A view's compiled code runs inside a framework method, in its **own scope**, after the App script and App header. So: - Variables from `App.pvue` are not visible in views. Use `backend/` singletons, `$_SESSION` or `define()`. - Output is buffered, so `header()`, `setcookie()`, redirects (`header('Location: /login'); exit;`) and `http_response_code()` all work from a view's ` ``` - Declaration (0.0.3): `@AJAX('POST')` or `@AJAX("GET")`, then `function name(...)`. Typed parameters, defaults and return types are fine: `@AJAX('POST') function save(array $input = []): void {`. - `$input` is the decoded JSON body for POST, or `$_GET` for GET (which also contains `page`). A function with no parameters is called with none. - The declared method is **enforced**: the other method gets `405` with an `Allow:` header and `{"error": "@AJAX 'name' only accepts POST"}`. - Only `@AJAX` functions can be called. If `action` names anything else, the request falls through and returns the normal HTML page (status 200), so check `response.ok` and the content type on the client. - **`echo`, don't `return`.** Set `Content-Type: application/json` and use `http_response_code()` for errors. - Handlers run **before** the App and view scripts: they cannot see variables from those scripts. Load what you need inside the handler (a `backend/` singleton, `$_SESSION`). - POST handlers always have a session (PHPue starts one, even with lazy sessions). GET handlers only have one if the visitor already has a session cookie; call `phpue_session(true)` before writing. - Function names are global across the whole app: keep them unique. - Handlers from App, every view and every component are available on every page, in source mode and in the built `.dist/`. Posting to the page that owns the handler (`fetch('')`) is the clearest convention. ## Client side ```js // POST: JSON body with an "action" async function saveNote(text) { const res = await fetch('', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'saveNote', text }) }); const data = await res.json(); if (!res.ok || !data.ok) throw new Error(data.error || 'Save failed'); return data; } // GET: query string const notes = await fetch('?action=listNotes').then(r => r.json()); ``` `fetch('')` posts to the current URL. Use `window.location.pathname` if the page has query parameters you do not want to resend. ## Submitting a form through @AJAX ```html async function submitForm(event, action) { event.preventDefault(); const fields = Object.fromEntries(new FormData(event.target)); const res = await fetch('', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, ...fields }) }); const data = await res.json(); document.getElementById('subscribeResult').textContent = data.message; } ``` ```php @AJAX('POST') function subscribe($input) { header('Content-Type: application/json'); $email = filter_var($input['email'] ?? '', FILTER_VALIDATE_EMAIL); if (!$email) { http_response_code(422); echo json_encode(['message' => 'Please enter a valid email.']); return; } // Newsletter::getInstance()->add($email); echo json_encode(['message' => 'Thanks, you are subscribed.']); } ``` ## The common pattern: mutate, then reload The simplest PHPue interaction loop needs almost no JavaScript: an `@AJAX` handler changes `$_SESSION` or the database, the client calls `location.reload()`, and the server renders the new state. For no-reload updates, echo JSON and patch the DOM in ``, or use AlpineJS ([client JS](/docs?topic=client-js)). ## File uploads and other non-JSON requests The dispatcher only reads JSON bodies for POST, so `FormData` / multipart uploads do not reach `@AJAX` (you get the HTML page back). Use a backend endpoint instead: ```php false]); exit; } // validate type and size, move_uploaded_file(...), store the result echo json_encode(['ok' => true]); exit; } ``` Client: `fetch('?api&endpoint=avatarUpload', {method: 'POST', body: new FormData(form)})`. ## Security checklist - Validate and authorise **inside every handler**; `$input` is untrusted user data. - Add CSRF protection for state-changing handlers on logged-in sites (a token stored in `$_SESSION`, sent in the JSON body, compared with `hash_equals`). - Escape anything you send back that the client will insert as HTML, or insert it with `textContent`. - GET handlers can be cached by proxies for anonymous visitors: send `Cache-Control: no-store` for per-user data. - On 0.0.2 the dispatcher calls **any** existing PHP function named in `action` (including built-ins) and ignores the declared method. Upgrade to 0.0.3. --- # backend/, scope, sessions and language > Every `*.php` file under `backend/` is `require_once`d on every request, before routing and `@AJAX` dispatch. Put classes, singletons, database access, API clients and helper functions there. App, views and `@AJAX` handlers each run in their own scope, so `backend/` singletons (or `$_SESSION`, `define()`) are how they share data. Sessions are controlled by `PHPUE_SESSION` (`'auto'` or `'lazy'`). ## backend/ autoloading - Files load recursively in sorted path order (0.0.3). If a folder contains `autoload.php`, **only that file** is loaded for the folder and PHPue does not scan deeper. Use it to fix load order (`extends` across files) or to keep data files out of the scan. - Files run on every request, including AJAX calls and 404s. Keep them to definitions and idempotent bootstraps. **Never echo** from them (except dedicated endpoint files that `exit`). - Composer: put `vendor/` inside `backend/` (its `autoload.php` stops the scan there). Do not add Composer at the project root. - A file that `return`s an array (config, language pack) must not be loaded twice with `require_once`, because the second call returns `true`. Keep such files in a folder with an `autoload.php`, or load them with `require`. ## The singleton pattern (author's convention) ```php pdo ??= new PDO('sqlite:' . getcwd() . '/data/app.sqlite', null, null, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); } public function latestPosts(int $limit = 10): array { $stmt = $this->pdo()->prepare('SELECT id, title, body FROM posts ORDER BY id DESC LIMIT :limit'); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } } ``` Use it from any view, component, App or `@AJAX` handler: `$posts = Database::getInstance()->latestPosts(5);`. Keep secrets in a git-ignored file (for example `backend/env.php` with `putenv('DB_PASS=...')` or `define()`), and use prepared statements for every query. ## Scope: who sees what | Code | Scope | |---|---| | `App.pvue` script + components placed in App | App scope | | A view's script + components placed in that view | that view's scope | | `@AJAX` functions | plain functions, called before the App/view scripts run | | `backend/` files | their classes, functions and constants are global; loose variables are not (expose data through a class) | Request order: backend files load, the session starts (per `PHPUE_SESSION`), `@AJAX` dispatch (exits if it matches), App script, ``, App template, then the view's script and template at ``. So an App variable is **undefined** in a view, and a handler cannot see page variables. Share data with: - a backend singleton (best: `Config::getInstance()->get('siteName')`), - `define('SITE_NAME', '...')` in the App script or backend, - `$_SESSION` (per visitor), - `$GLOBALS['x']` (request-wide, use sparingly). ## Sessions: `PHPUE_SESSION` Set in `index.php`: `define('PHPUE_SESSION', 'lazy');` | Mode | Behaviour | Use for | |---|---|---| | `'auto'` (default) | `session_start()` on every request, like 0.0.2 | apps where every visitor has state | | `'lazy'` | only resumes a session the visitor already has; anonymous pages send no `Set-Cookie` and stay cacheable | public sites, docs, blogs, anything behind a cache or CDN | With `'lazy'`: - Reading `$_SESSION` needs nothing: it is simply empty for new visitors. - Before **writing** during page rendering (or in a GET handler, or backend code), call `phpue_session(true)`. - POST `@AJAX` handlers start a session automatically. - Extensions and backend code should call `phpue_session()` / `phpue_session(true)` instead of `session_start()`. - If the App script text contains `session_start()` (even in a comment), PHPue's session handling switches off entirely. ```php // a view that remembers the last search (lazy sessions) $query = trim($_GET['q'] ?? ''); if ($query !== '') { phpue_session(true); $_SESSION['lastQuery'] = $query; } $lastQuery = $_SESSION['lastQuery'] ?? ''; ``` ## Language: `` Priority per request: `phpue_set_lang('cy')` > the view header's `` > `PHPUE_LANG` (`'en'` in `index.php`). `phpue_html_lang()` returns the resolved value. Call `phpue_set_lang()` in the **App script** or a `backend/` bootstrap. A view runs after `` is printed, so calling it there is too late. ## File paths `__DIR__` inside `.pvue` code is `www/` in source mode but `www/.dist` or `www/.dist/pages` after a build, so paths built from it break in production. Use `getcwd()`, which is always the web root: ```php $posts = json_decode(file_get_contents(getcwd() . '/data/posts.json'), true); ``` Inside `backend/*.php` files, `__DIR__` is that file's real folder and is safe to use for files next to it (the build copies `backend/` into `.dist/backend/`). ## Other runtime helpers (0.0.3) - `phpue_session(bool $write = false)`: resume, or with `true` start, the session. - `phpue_set_lang(string $lang)` / `phpue_html_lang()`. - `phpue_iterable($x)`: returns `$x` if iterable, otherwise `[]` (what `p-for` uses). - `phpue_navigation()`, `get_current_route()`, `phpue_current_meta()`: see [routing](/docs?topic=routing). --- # cscript, AlpineJS, x-ssr and HTMX > PHPue renders everything on the server; the browser layer is optional. Use `` for plain JavaScript (PHP values arrive through `{{ $var }}`, which is `json_encode`d). Add AlpineJS when a page needs client-side state, and the official **x-ssr** Alpine extension when Alpine must update a list that PHP already rendered. HTMX works for SPA-like navigation without leaving server rendering. ## PHP data in `` ```html const count = {{ $count }}; // 3 const fruits = {{ $fruits }}; // ["Apple","Pear"] const user = {{ $user }}; // {"name":"Ada"} const items = {{ $data['items'] }}; // any $ expression (0.0.3) const total = ; // function calls: json_encode yourself ``` - `{{ $expr }}` becomes `json_encode($expr, JSON_HEX_TAG|JSON_HEX_APOS|JSON_HEX_QUOT|JSON_HEX_AMP)`. **Never wrap it in quotes**: `"{{ $name }}"` gives `""Ada""`. - Function calls are not converted inside cscript; use ``. - A cscript with a line starting `import`/`export` becomes `
document.addEventListener('alpine:init', () => { Alpine.data('counter', () => ({ count: {{ $start }}, async increment() { this.count++; await fetch('', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'saveCount', count: this.count }) }); } })); }); ``` Rules: - All Alpine attributes pass through untouched: `x-data`, `x-show`, `x-text`, `x-model`, `:class`, `@click`, `