Separate contact-form logic from presentation in MODX Revolution
Use uncacheable Snippets for form handling, cached Chunks for markup, and xPDO for database access in MODX Revolution.
02 Feb 2026, 01:40 UTC

The problem: HTML trapped inside PHP
It is common to inherit a MODX site where a contact form lives entirely inside a Snippet as a long PHP string. The markup is hard for content editors to change, the Snippet becomes difficult to test, and every form submission re-parses PHP instead of reusing cached HTML. MODX Revolution already has tools to split these concerns: Snippets for PHP logic, Chunks for HTML, and xPDO for database access.
The useful takeaway: keep the Snippet small and uncacheable, let it return a Chunk, and put all presentation in that Chunk. This makes the form easier to maintain and keeps caching under control.
Split the job: Snippet handles logic, Chunk handles markup
In MODX Revolution, a Snippet is PHP code executed while a page is parsed. A Chunk is a reusable HTML template with placeholders such as [[+name]]. A Template Variable lets editors supply per-resource values, like a contact email address. xPDO is MODX's object-relational layer for querying resources, users, or custom tables without writing raw SQL.
Call the Snippet uncacheable with [[!contactForm]] so it executes on every request, including form POSTs. The Chunk it returns can still contain cached markup because the Snippet only decides which placeholder values to inject.
Worked example: contact form
In the Manager, go to Elements > Snippets and create a new Snippet named contactForm. Use a Template Variable named contactEmail so editors can set the recipient per resource.
<?php
/* contactForm Snippet */
$emailTv = $modx->getOption('emailTv', $scriptProperties, 'contactEmail');
$success = false;
$errors = [];
$name = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$message = trim($_POST['message'] ?? '');
if ($name === '') {
$errors[] = 'Please enter your name.';
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Please enter a valid email address.';
}
if ($message === '') {
$errors[] = 'Please enter a message.';
}
if (empty($errors)) {
$recipient = $modx->resource->getTVValue($emailTv);
if ($recipient === '') {
$recipient = $modx->getOption('emailsender');
}
/* xPDO example: save to a custom mapped class if you have one.
Replace ContactSubmission with your own class or remove this block. */
if ($modx->loadClass('ContactSubmission', MODX_CORE_PATH . 'model/contact/', false, true)) {
$submission = $modx->newObject('ContactSubmission');
$submission->set('name', $name);
$submission->set('email', $email);
$submission->set('message', $message);
$submission->save();
}
$success = true;
}
}
$errorList = '';
if (!empty($errors)) {
$errorList = '<ul class="errors">';
foreach ($errors as $error) {
$errorList .= '<li>' . htmlspecialchars($error, ENT_QUOTES, 'UTF-8') . '</li>';
}
$errorList .= '</ul>';
}
return $modx->getChunk('contactFormResult', [
'success' => $success ? '1' : '0',
'errors' => $errorList,
'name' => htmlspecialchars($name, ENT_QUOTES, 'UTF-8'),
]);
?>
Create a Chunk named contactFormResult:
<h2>Contact</h2>
[[+success:eq=`1`:then=`
<p>Thanks, [[+name]]. Your message was sent.</p>
`:else=`
<form method="post" action="[[~[[*id]]]]">
[[+errors]]
<label>Name</label><br>
<input type="text" name="name" value="[[+name]]"><br>
<label>Email</label><br>
<input type="email" name="email"><br>
<label>Message</label><br>
<textarea name="message"></textarea><br>
<button type="submit">Send</button>
</form>
`]]
Place [[!contactForm]] in the contact resource where the form should appear. Because the Snippet is uncacheable, MODX runs it on each request and injects the result into the page.
Caching decision and trade-offs
An uncacheable Snippet prevents the containing resource from being cached, so every visitor triggers PHP execution. For a low-traffic contact page that is usually acceptable. On a high-traffic site, move the form to a dedicated AJAX handler or keep the uncacheable Snippet on a lightly visited page only.
Avoid building HTML inside PHP strings. If the markup lives in the Snippet, editors cannot edit it through the Manager and you bypass MODX's Chunk parsing layer. Also, never interpolate user input directly into xPDO queries; use bound parameters or getObject/getCollection with criteria arrays.
Verify before you rely on it
- Open Reports > System Info in the Manager and confirm your MODX Revolution version. Code written for 2.x may need review on 3.x because of class and namespace changes.
- Create a test Snippet that returns a Chunk placeholder, call it as
[[!TestSnippet]], and reload the page to confirm it runs uncached each time. - Submit the contact form with blank and invalid values. Confirm validation messages appear and no stack traces are shown to visitors.
- Check the MODX error log after submission to verify xPDO writes succeeded without warnings.
Limitation
Snippets run in the web request context. A long-running task such as sending bulk email should be deferred to a CLI script or queue, not executed inside a page-rendering Snippet.
Closing
Start with one contact page. Create the Snippet, create the Chunk, call the Snippet uncacheably, and test submissions. If you ever need to undo the change, delete the Snippet and Chunk from Elements in the Manager.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.