For the complete documentation index, see llms.txt. This page is also available as Markdown.

Server-Side PHP Components

Advanced PHP patterns — runtime rendering, @raw tokens, caching, and requiresPhp

Most components finish their work before the site is published: hooks compute values, templates resolve, and what ships is static HTML. A server-side component keeps working after publish — its output is PHP that runs on the hosting server every time a visitor requests the page. That unlocks live data (a spreadsheet that updates without republishing), content that varies per visitor, and logic that must never run in the browser. This guide covers the patterns that make those components solid: where server-side code can live, what requiresPhp really does, how build-time directives and request-time PHP coexist in one file, protecting runtime tokens with @raw, caching remote data, and gating content behind a session.

Two Kinds of Server-Side Code

A .php main template. A component can use templates/index.php in place of index.html as its main template. The file is processed by the Elements template engine at publish like any other template, but the PHP inside it is deployed to the server and runs when the published page is requested — its output becomes part of the page itself. This is the right home for server-rendered markup: tables built from remote data, per-visitor content, anything where the page's HTML depends on request-time state. The PHP Templates reference covers the mechanics — property insertion into PHP, the closure and heredoc patterns, and why edit mode needs a PHP-free branch.

templates/backend/ endpoints. Files in the backend directory are not part of the page output. They deploy to a per-page backend folder and are called separately — typically via fetch() from your front-end JavaScript — to handle form submissions, AJAX requests, and API-style work. The Backend reference documents the deployment structure, node.backendPath, and a complete contact-form example. This guide doesn't repeat that material; everything below is about the first kind — components whose page output is PHP — plus the patterns both kinds share.

Declaring requiresPhp

A .php template only executes if the page that contains it is itself served as PHP. That's what the requiresPhp key in info.json enforces: set it to true if your component outputs server-side PHP that needs to run on the hosting page, and the page containing the component is published with a .php filename — otherwise the PHP won't be executed. It defaults to false, and per the reference it's not needed for child components that only ever appear nested inside another component that already requires PHP.

The Table declares it like this:

// info.json
{
    "identifier": "com.realmacsoftware.table",
    "title": "Table",
    "group": "Content",
    "icon": "icon",
    "requiresPhp": true,
    "helpURL": "https://docs.realmacsoftware.com/elements-docs/elements-app/components/built-in-components/table"
}

The child-component rule matters most when you build a suite. Imagine a members-area suite: a Members Area parent with requiresPhp: true, and children — Member Greeting, Logout Button — designed to be dropped inside it. Only the parent needs the flag: any page holding the parent is already published as .php, so children nested inside it get a PHP-capable page for free. But the inherited requirement doesn't travel. If the Logout Button can also be placed standalone — say, in a site-wide navigation bar on a page with no Members Area — it must declare requiresPhp: true itself, or its PHP will ship inside an .html page and appear as literal source text.

Worked Example: The Table's index.php

The Table renders CSV data — a local file or a remote URL — into a styled, sortable table on the server, so the published page always reflects the current spreadsheet. Its main template is templates/index.php, and it's the best available example of build-time Elements directives and request-time PHP sharing one file. (Its edit-mode branch, which swaps in example rows because PHP can't run in the editor, is covered in PHP as the Main Template — we'll focus on the published-page path.)

Hooks Prepare Everything PHP Will Need

PHP can't reach into rw.props, so the hooks file resolves every setting to a plain string or JSON blob before the template is processed. Condensed from hooks.source.js:

Three details are deliberate. csvSource is normalised to a single string whichever source type the user picked, so the template needs no source-type logic. Booleans destined for PHP are serialised as the strings "true"/"false" — PHP will re-parse them, because a {{property}} interpolation is always text by the time PHP sees it. And structured data (csvColumnMeta) travels as a JSON string, to be decoded server-side from a heredoc.

One File, Two Execution Phases

Here is the heart of the published-page branch, trimmed from templates/index.php:

Read this as two passes over the same text:

  1. Publish time (Elements). The template engine resolves everything it recognises: @if(showSearch) keeps or drops the search block, and every {{…}} becomes a literal value — {{id}}, {{alpineConfig}}, {{classes.searchInput}}, {{csvSource}}, {{classes.tr}}. The <?php … ?> blocks are just opaque text to Elements; they pass through untouched (as does Alpine's @input/@click shorthand, which isn't an Elements directive). The result — now pure PHP-plus-HTML with no directives left — is written into the published site with a .php filename, courtesy of requiresPhp.

  2. Request time (PHP). Each time a visitor loads the page, the server executes what publish time produced. $csvSource = '…'; now assigns the resolved URL, and the foreach stamps out one <tr> per CSV row.

The foreach loop is where the two phases visibly interlock: {{classes.tr}} was interpolated once at publish, so at request time PHP echoes that same literal class string on every iteration. Build-time values are frozen into the PHP source; request-time values flow through PHP variables. Once that distinction is comfortable, files like this stop looking exotic.

Fetching the CSV at Request Time

When the source is a URL, the Table fetches it fresh on every request — this excerpt runs inside the <?php block elided above:

Two lessons here. First, hosting environments differ: allow_url_fopen is off on plenty of shared hosts, so the curl fallback isn't paranoia, it's compatibility. Second, failure is a first-class outcome — $csvError drives a friendly "Unable to load CSV data" table instead of a blank page or a PHP warning. (The full file also rewrites Google Sheets share URLs into their CSV-export form before fetching — worth reading in the source.) What the Table deliberately doesn't do is cache: every request pays for the fetch. For a table that's fine; for slower APIs, see Caching Remote Data on the Server below.

Runtime Token Substitution with @raw

The Table interleaves PHP echo statements with markup. An alternative pattern — common in data-driven components — is to keep a clean markup sub-template with {{name}}-style tokens, and have PHP fill the tokens in a loop with str_replace at request time. One template string, one substitution call per row, no PHP tangled through the markup.

The catch: {{name}} is exactly the syntax Elements resolves at publish. Left unprotected, Elements would try to find a property called name and replace it — your tokens are consumed at build, PHP's str_replace finds nothing to replace at request time, and every card renders blank. The fix is the @raw directive: content between @raw() and @endraw skips template processing entirely and ships to the server as literal text. And note it's every literal occurrence of the token text that needs protecting — including the token list inside your PHP str_replace call, not just the markup.

Here's the pattern as a fresh component, com.example.productlist, which renders product cards from a remote JSON API at request time (its info.json sets requiresPhp: true):

The split is the whole trick. Everything Elements should resolve — the API URL, the cache TTL, the class strings — is assigned to PHP variables outside the @raw block, so it's frozen into the deployed file at publish. Everything PHP owns — the card template with its {{name}}/{{price}}/{{image}} tokens, the fetch, the loop — sits inside @raw, so it reaches the server byte-for-byte intact. The heredoc bridges the two: PHP interpolates $cardClasses at request time, while the {{…}} tokens wait for str_replace. (If you'd rather keep Elements values inline in the raw markup instead of routing them through variables, @raw blocks can be exited and re-entered mid-stream — see Combining with Elements Properties.)

Tailwind Can't See Request-Time Classes

One side effect of rendering markup at request time: Tailwind's JIT engine only compiles classes it finds in your template files at build or preview time. The classes above are safe — they're literal strings in hooks.js and the template — but any Tailwind class that first appears at request time (composed by PHP logic, or arriving from the API data) will have no CSS behind it. The documented workaround is to list those classes in an HTML comment in a template file so the JIT scan picks them up — see Troubleshooting: Dynamic PHP Code and Tailwind Classes Generation.

Caching Remote Data on the Server

Fetching a remote API on every page view makes your page as slow — and as fragile — as the API. A small file cache with a TTL fixes both: fetch at most once every N minutes, and if the remote is down, serve the last good copy instead of an empty page. Here's the $fetchJsonCached helper the product list uses; paste it at the top of the same @raw PHP block, before the card template (it contains no {{…}} tokens, so @raw neither helps nor hurts it — placement inside is simply convenient):

Details worth keeping: the cache lives in sys_get_temp_dir(), which is writable on virtually every host and outside the web root, so the raw cache file is never served publicly; the key is an md5 of the URL, so two components fetching different feeds never collide; and a response is only cached if it actually parses as JSON, so one bad gateway page can't poison the cache. The closure-not-named-function rule is the same one the Table follows — root-level templates are processed once per instance, and named functions would be declared twice when two instances share a page. If your data outgrows a flat JSON file — thousands of rows, per-record lookups, concurrent writes — PHP's bundled SQLite via PDO is the natural next step, and the caching idea carries over unchanged.

Session-Gated Content

Because a .php main template runs per request, it can also decide whether to render at all. The sketch below wraps a dropzone in a PHP session check on the published site, while leaving it always visible (and editable) in the editor — note how the @if(!edit) blocks emit only the PHP wrapper around the dropzone, so the dropzone itself appears exactly once in the template:

At publish, the editor path drops the PHP entirely; the published path becomes a plain PHP if/else around the dropzone's rendered children. Two scope notes. session_start() must run before the page sends output, so a component like this belongs early in the page — or better, let the login endpoint own session creation and keep the template to reading $_SESSION. And the login itself — the form, password hashing, verification, setting the session flag — is a job for a templates/backend/ endpoint, which is exactly the request/response flow the Backend reference documents.

Paths Cheat-Sheet

Server-side components juggle more paths than most. These are the documented members, from the rw.node, rw.page, and rw.component reference pages:

Member
Documented meaning
Reach for it when…

rw.node.backendPath

Path to the node's backend folder

your front-end fetch()es a templates/backend/ endpoint — this is the per-page folder those files deploy into

rw.page.absolutePath

Full path to the page

you need the location of the page itself, e.g. building a link or redirect back to it

rw.page.docRootPath

Path to document root

server-side code needs to reach files elsewhere in the published site, relative to the site root

rw.component.assetPath

Path to component-specific assets

referencing files bundled with this component (images, scripts, styles)

rw.component.siteAssetPath

Path to site-level component assets

require_once-ing a shared PHP library deployed with the site — the pattern both PHP reference pages use

rw.component.sharedAssetPath

Path to shared pack assets

referencing files shared across the whole Element Pack

To use any of these in a template, pass them through from hooks.jsrw.setProps({ node: rw.node, siteAssetPath: rw.component.siteAssetPath }) — as the Backend reference shows; see Passing Data to Templates.

A Note on Escaping

  • PHP Templates — root-level PHP and index.php as the main template

  • Backend Directory — request handlers called via fetch()

  • @raw — shipping literal {{…}} tokens through the build

  • info.jsonrequiresPhp and the component manifest

  • External Data Sources — pulling remote data into components

  • Building Form Components — inputs, validation, and submission handling

  • Troubleshooting — the PHP + Tailwind JIT gotcha

Last updated

Was this helpful?