PHP API
The TotalCMS class is the main entry point for using Total CMS from PHP. It can be used both for rendering pages with Twig templates and for writing standalone CLI automation scripts.
Page Rendering
The simplest way to build a page is to initialize TotalCMS at the top of your PHP file, write your HTML with Twig macros inline, and call processBufferMacros() at the very end. The constructor starts output buffering automatically, so everything between the opening PHP tag and the final processBufferMacros() call is captured and processed through Twig.
Custom templates should go into tcms-data/templates. Global templates that can be used include totalform.twig.
Basic Page Structure
<?php
require_once __DIR__ . '/../vendor/autoload.php';
$totalcms = new TotalCMS\TotalCMS();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ cms.text('sitetitle') }} - My Site</title>
<link rel="stylesheet" href="{{ cms.api }}/assets/content.css?v={{ cms.version }}"/>
</head>
<body>
<h1>{{ cms.text('sitetitle') }}</h1>
<!-- Display an image with resize options -->
<div class="hero">
{{ cms.image('hero', {w: 1200, h: 400, fit: 'crop'}) }}
</div>
<!-- List objects from a collection -->
{% set posts = cms.objects('blog', {featured: true}) | slice(0, 3) %}
{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<span>By {{ post.author }} • {{ post.date | date('M j, Y') }}</span>
<p>{{ post.summary | striptags | truncate(150) }}</p>
{% if post.tags | length > 0 %}
<span class="badge">{{ post.tags[0] }}</span>
{% endif %}
{{ cms.image(post.id, {w: 400, h: 200, fit: 'crop'}, {collection: 'blog'}) }}
</article>
{% endfor %}
<!-- Gallery with lightbox -->
{{ cms.gallery('photos', {
columns: 3,
gap: 1.5,
lightbox: true,
thumbnailWidth: 400,
thumbnailHeight: 300,
thumbnailFit: 'crop'
}) }}
<!-- CMS Grid for structured layouts -->
{% cmsgrid cms.objects('blog') | slice(0, 5) from 'blog' with 'list' %}
<div class="cms-image">
{{ cms.image(object.id, {w: 400, h: 400, fit: 'crop'}, {collection: 'blog'}) }}
</div>
<div class="cms-content">
<h3>{{ object.title }}</h3>
<p>{{ object.summary | striptags | truncate(200) }}</p>
</div>
{% endcmsgrid %}
<!-- Various field types -->
<p>Email: {{ cms.email('contact') }}</p>
<p>URL: <a href="{{ cms.url('website') }}">{{ cms.url('website') }}</a></p>
<p>Price: ${{ cms.number('price') }}</p>
<p>Date: {{ cms.date('published') | date('F j, Y') }}</p>
<p>Color: {{ cms.color('brand') | hex }}</p>
<p>Toggle: {% if cms.toggle('active') %}Enabled{% else %}Disabled{% endif %}</p>
<div>{{ cms.styledtext('about') }}</div>
<!-- Total CMS Scripts -->
<script type="module" src="{{ cms.api }}/assets/content.js?v={{ cms.version }}"></script>
<script type="module" src="{{ cms.api }}/assets/gallery.js?v={{ cms.version }}"></script>
</body>
</html>
<?php echo $totalcms->processBufferMacros(); ?>
Reducing Time to First Byte
For large pages, you can flush the <head> early so the browser can start loading stylesheets and scripts while the rest of the page is still rendering:
<?php
require_once __DIR__ . '/../vendor/autoload.php';
$totalcms = new TotalCMS\TotalCMS();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ cms.text('sitetitle') }}</title>
<link rel="stylesheet" href="{{ cms.api }}/assets/content.css?v={{ cms.version }}"/>
</head>
<?php
// Flush the head to the browser immediately
echo $totalcms->processBufferMacros();
$totalcms->startBuffer();
?>
<body>
<!-- Rest of page content... -->
</body>
</html>
<?php echo $totalcms->processBufferMacros(); ?>
Common Twig Variables and Functions
<!-- Global CMS variables -->
{{ cms.api }} {# API base URL #}
{{ cms.version }} {# Total CMS version #}
{{ cms.env }} {# Current environment #}
{{ cms.config('key') }} {# Configuration values #}
<!-- Content functions -->
{{ cms.text('id') }}
{{ cms.email('id') }}
{{ cms.url('id') }}
{{ cms.number('id') }}
{{ cms.date('id') }}
{{ cms.color('id') }}
{{ cms.toggle('id') }}
{{ cms.styledtext('id') }}
{{ cms.image('id', {w: 600, h: 500}) }}
{{ cms.imagePath('id', {w: 600, h: 500}) }}
{{ cms.alt('id') }}
{{ cms.gallery('id', {columns: 3}) }}
<!-- Collection access -->
{% set objects = cms.objects('collection') %}
{% set object = cms.object('collection', 'id') %}
{% set values = cms.property('collection', 'field') %}
{{ cms.data('collection', 'id', 'field') }}
<!-- Depot files -->
{% for file in cms.depot('id') %}
{{ file.name }} - {{ file.uploadDate | date('c') }}
{% endfor %}
<!-- Color filters -->
{{ cms.color('id') | color }} {# CSS-ready color value #}
{{ cms.color('id') | oklch }}
{{ cms.color('id') | rgb }}
{{ cms.color('id') | hsl }}
{{ cms.color('id') | hex }}
CLI Automation Scripts
The TotalCMS class is equally powerful for writing standalone PHP scripts that run from the command line. Use it to build cron jobs, data processing scripts, bulk operations, and other automation tasks.
Getting Started
When running from CLI, set DOCUMENT_ROOT so Total CMS can locate your data directory, then require the autoloader:
<?php
use Monolog\Level;
use TotalCMS\TotalCMS;
// Set DOCUMENT_ROOT for CLI mode
$_SERVER['DOCUMENT_ROOT'] = __DIR__ . '/../public';
require_once __DIR__ . '/../public/vendor/autoload.php';
$totalcms = new TotalCMS();
In CLI mode, the constructor automatically skips session and buffer initialization since they are not needed.
Logging
Create a named logger for your script. Logs appear in the Log Analyzer in the admin dashboard.
$logger = $totalcms->createLogger(
name: 'my-script',
console: true, // Also output to stdout
level: Level::Debug // Log level (null uses system default)
);
$logger->info("Script started");
$logger->debug("Processing item...");
$logger->error("Something went wrong");
Cache Control
// Clear all caches before processing (ensures fresh data)
$totalcms->clearCache();
// Or disable cache reads for the entire process
// (writes still occur to warm shared caches with fresh data)
$totalcms->disableCache();
Reading Data
Fetching Collection Indexes
Use indexReader() to get a collection's index, which contains lightweight references to all objects.
$indexReader = $totalcms->indexReader();
$index = $indexReader->fetchIndex('products');
$count = $index->objects->count();
$logger->info("Found {$count} products");
// Iterate over index entries
$index->objects->each(function ($item) {
$id = $item["id"];
// Process each item...
});
Fetching Full Objects
Use objectFetcher() to retrieve complete objects with all their data.
$objectFetcher = $totalcms->objectFetcher();
$product = $objectFetcher->fetchObject('products', 'widget-pro');
$data = $product->toArray();
echo $data['title']; // "Widget Pro"
echo $data['price']; // 29.99
Searching Indexes
$indexSearcher = $totalcms->indexSearcher();
// Search within a collection's index
Fetching Properties
$propertyFetcher = $totalcms->propertyFetcher();
// Retrieve specific properties from objects
Writing Data
Creating Objects
$objectSaver = $totalcms->objectSaver();
$objectSaver->saveObject('blog', [
'id' => 'my-new-post',
'title' => 'Hello World',
'body' => 'This is my first post.',
]);
Updating Objects
$objectUpdater = $totalcms->objectUpdater();
$objectUpdater->updateObject('blog', 'my-new-post', [
'title' => 'Updated Title',
]);
Removing Objects
$objectRemover = $totalcms->objectRemover();
$objectRemover->removeObject('blog', 'my-new-post');
Cloning Objects
$objectCloner = $totalcms->objectCloner();
$objectCloner->cloneObject('blog', 'my-post', 'my-post-copy');
Incrementing Properties
$incrementer = $totalcms->propertyIncrementer();
$incrementer->incrementProperty('products', 'widget-pro', 'stock', 10);
$incrementer->decrementProperty('products', 'widget-pro', 'stock', 1);
Deck Items
Deck properties are key-value maps stored within an object (e.g., line items, tags, entries). Use the deck services to manage individual items without rewriting the entire object.
// Add a new deck item
$totalcms->deckItemSaver()->saveDeckItem(
collection: 'orders',
objectId: 'order-123',
propertyName: 'line_items',
itemId: 'item_001',
itemData: [
'product' => 'widget-pro',
'quantity' => 2,
'price' => 29.99,
]
);
// Update an existing deck item
$totalcms->deckItemUpdater()->updateDeckItem('orders', 'order-123', 'line_items', 'item_001', [
'quantity' => 3,
]);
// Fetch a specific deck item
$item = $totalcms->deckItemFetcher()->fetchDeckItem('orders', 'order-123', 'line_items', 'item_001');
// Remove a deck item
$totalcms->deckItemRemover()->removeDeckItem('orders', 'order-123', 'line_items', 'item_001');
Schemas and Collections
// List all available schemas
$schemas = $totalcms->schemaLister()->listSchemas();
// Fetch a specific schema definition
$schema = $totalcms->schemaFetcher()->fetchSchema('blog');
// List all collections
$collections = $totalcms->collectionLister();
// Fetch collection metadata
$collection = $totalcms->collectionFetcher();
// Rebuild a collection's index
$totalcms->indexBuilder()->rebuildIndex('blog');
Send emails using configured mailer templates.
$totalcms->mailer()->sendEmail('order-confirmation', [
'orderId' => 'order-123',
'total' => '$59.98',
]);
Job Queue
Run background jobs programmatically.
$jobRunner = $totalcms->jobRunner();
Files and Images
// Save files and images programmatically
$totalcms->fileSaver()->saveFile('documents', 'doc-id', 'file', $uploadedFile);
$totalcms->imageSaver()->saveImage('gallery', 'gallery-id', 'image', $uploadedFile);
// Get filesystem paths
$path = $totalcms->filePath('my-document');
$depotFile = $totalcms->depotPath('my-depot', 'reports/annual.pdf');
Error Handling with Email Notifications
Wrap your script logic in a try/catch and use the mailer to send error notifications.
try {
// ... your script logic ...
} catch (Throwable $e) {
$logger->error("An error occurred: " . $e->getMessage());
$totalcms->mailer()->sendEmail('script-errors', [
'job' => 'my-script',
'error' => $e->getMessage(),
]);
exit(1);
}
exit(0);
Complete Example: Nightly Data Processing Script
This example shows a typical automation pattern — iterating over a collection, reading related data, performing calculations, and writing results back.
<?php
/**
* Process Monthly Subscriptions
*
* Creates billing records for active subscribers.
*
* Usage:
* php processSubscriptions.php # Process for today
* php processSubscriptions.php 2026-01-15 # Process for specific date
*/
use Monolog\Level;
use TotalCMS\TotalCMS;
$_SERVER['DOCUMENT_ROOT'] = __DIR__ . '/../public';
require_once __DIR__ . '/../public/vendor/autoload.php';
$totalcms = new TotalCMS();
$totalcms->clearCache();
$logger = $totalcms->createLogger(name: 'processSubscriptions', console: true, level: Level::Debug);
// Parse optional date argument
$targetDate = $argv[1] ?? date('Y-m-d');
$logger->info("Processing subscriptions for {$targetDate}");
try {
$indexReader = $totalcms->indexReader();
$objectFetcher = $totalcms->objectFetcher();
$deckItemSaver = $totalcms->deckItemSaver();
$created = 0;
$skipped = 0;
// Fetch all members
$membersIndex = $indexReader->fetchIndex('members');
$logger->info("Found {$membersIndex->objects->count()} members");
$membersIndex->objects->each(function ($memberItem) use (
$objectFetcher, $deckItemSaver, $logger, $targetDate, &$created, &$skipped
) {
$memberId = $memberItem["id"];
try {
$member = $objectFetcher->fetchObject('members', $memberId)->toArray();
} catch (Throwable $e) {
$logger->error("Failed to fetch member {$memberId}: " . $e->getMessage());
return; // Skip this member
}
// Check if member has an active subscription
$plan = $member["plan"] ?? null;
if ($plan === null || ($member["status"] ?? '') !== 'active') {
$skipped++;
return;
}
// Generate a deterministic ID to prevent duplicates
$billingId = $memberId . "_" . str_replace('-', '_', substr($targetDate, 0, 7));
// Check if already billed this period
$billings = $member["billings"] ?? [];
if (isset($billings[$billingId])) {
$skipped++;
return;
}
// Create billing record
$deckItemSaver->saveDeckItem(
collection: 'members',
objectId: $memberId,
propertyName: 'billings',
itemId: $billingId,
itemData: [
"amount" => $member["plan_price"] ?? 0,
"created" => $targetDate,
"plan" => $plan,
"status" => "pending",
]
);
$created++;
$logger->info("Created billing {$billingId} for {$memberId}");
});
$logger->info("Complete. Created: {$created}, Skipped: {$skipped}");
} catch (Throwable $e) {
$logger->error("An error occurred: " . $e->getMessage());
$totalcms->mailer()->sendEmail('script-errors', [
'job' => 'processSubscriptions',
'error' => $e->getMessage(),
]);
exit(1);
}
exit(0);
Page Access Control
These methods are available for web pages (not CLI scripts).
// Restrict page to logged-in users (redirects to login if not authenticated)
$totalcms->restrictPageAccess();
// Restrict to specific groups
$totalcms->restrictPageAccess(['admin', 'editor']);
// Check if user is logged in
if ($totalcms->isUserLoggedIn()) {
$user = $totalcms->userData();
echo "Welcome, " . $user['name'];
}
// Disable browser caching for authenticated users
$totalcms->noCacheIfAuthenticated();
Sitemaps
Generate XML sitemaps for collections.
// Get sitemap XML as a string
$xml = $totalcms->sitemapForCollection('blog', ['baseUrl' => 'https://example.com']);
// Or create a PSR-7 response
$response = $totalcms->createSitemapResponse('blog', ['baseUrl' => 'https://example.com']);
Method Reference
Constructor
| Method | Description |
|---|---|
new TotalCMS(bool $autoStartBuffer = true) |
Initialize Total CMS. In CLI mode, session and buffer are skipped automatically. |
Service Accessors
| Method | Returns | Description |
|---|---|---|
collectionLister() |
CollectionLister |
List available collections |
collectionFetcher() |
CollectionFetcher |
Fetch collection metadata |
indexReader() |
IndexReader |
Read collection indexes |
indexSearcher() |
IndexSearcher |
Search within indexes |
indexBuilder() |
IndexBuilder |
Rebuild collection indexes |
objectFetcher() |
ObjectFetcher |
Fetch full objects |
objectSaver() |
ObjectSaver |
Create new objects |
objectUpdater() |
ObjectUpdater |
Update existing objects |
objectRemover() |
ObjectRemover |
Delete objects |
objectCloner() |
ObjectCloner |
Clone/duplicate objects |
propertyFetcher() |
PropertyFetcher |
Fetch object properties |
propertyIncrementer() |
ObjectPropertyIncrementer |
Increment/decrement numeric properties |
deckItemSaver() |
DeckItemSaver |
Add items to deck properties |
deckItemUpdater() |
DeckItemUpdater |
Update deck items |
deckItemRemover() |
DeckItemRemover |
Remove deck items |
deckItemFetcher() |
DeckItemFetcher |
Fetch specific deck items |
schemaFetcher() |
SchemaFetcher |
Fetch schema definitions |
schemaLister() |
SchemaLister |
List available schemas |
fileSaver() |
FileSaver |
Save files programmatically |
imageSaver() |
ImageSaver |
Save images programmatically |
mailer() |
EmailService |
Send emails via templates |
jobRunner() |
JobRunner |
Run background jobs |
Logging
| Method | Returns | Description |
|---|---|---|
createLogger(string $name, bool $console = false, ?Level $level = null) |
LoggerInterface |
Create a named logger for custom scripts |
Cache Control
| Method | Returns | Description |
|---|---|---|
clearCache() |
array |
Clear all caches |
disableCache() |
void |
Disable cache reads for current process |
Page Access
| Method | Returns | Description |
|---|---|---|
restrictPageAccess(array\|string $groups = [], string $collection = '') |
void |
Restrict page to authenticated users/groups |
isUserLoggedIn(string $collection = '') |
bool |
Check if a user is logged in |
userData() |
array |
Get the logged-in user's data |
noCacheIfAuthenticated(string $collection = '') |
void |
Disable browser caching for logged-in users |
Buffer and Rendering
| Method | Returns | Description |
|---|---|---|
startBuffer() |
void |
Start output buffering |
endBuffer() |
void |
End output buffering |
processBufferMacros(array $data = [], bool $restartBuffer = false) |
string |
Process buffered content through Twig |
processMacros(string $templateName, array $data = []) |
string |
Render a named Twig template |
Sitemaps
| Method | Returns | Description |
|---|---|---|
sitemapForCollection(string $collection, array $options = []) |
string |
Generate sitemap XML |
createSitemapResponse(string $collection, array $options = []) |
ResponseInterface |
Create a PSR-7 sitemap response |
File Paths
| Method | Returns | Description |
|---|---|---|
filePath(string $id, array $options = []) |
?string |
Get filesystem path for a file property |
depotPath(string $id, string $filePath, array $options = []) |
?string |
Get filesystem path for a depot file |
Static Methods
| Method | Returns | Description |
|---|---|---|
TotalCMS::isPreview() |
bool |
Check if running in preview mode |
Public Properties
| Property | Type | Description |
|---|---|---|
config |
Config |
Access to the Total CMS configuration object |