Skip to content

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.

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.

<?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.render.image('hero', {w: 1200, h: 400, fit: 'crop'}) }}
</div>
<!-- List objects from a collection -->
{% set posts = cms.collection.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.render.image(post.id, {w: 400, h: 200, fit: 'crop'}, {collection: 'blog'}) }}
</article>
{% endfor %}
<!-- Gallery with lightbox -->
{{ cms.render.gallery('photos', {
columns: 3,
gap: 1.5,
lightbox: true,
thumbnailWidth: 400,
thumbnailHeight: 300,
thumbnailFit: 'crop'
}) }}
<!-- CMS Grid for structured layouts -->
{% cmsgrid cms.collection.objects('blog') | slice(0, 5) from 'blog' with 'list' %}
<div class="cms-image">
{{ cms.render.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(); ?>

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(); ?>
<!-- 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.render.image('id', {w: 600, h: 500}) }}
{{ cms.media.imagePath('id', {w: 600, h: 500}) }}
{{ cms.render.alt('id') }}
{{ cms.render.gallery('id', {columns: 3}) }}
<!-- Collection access -->
{% set objects = cms.collection.objects('collection') %}
{% set object = cms.collection.object('collection', 'id') %}
{% set values = cms.collection.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 }}

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.

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.

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");
// 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();

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...
});

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
$indexSearcher = $totalcms->indexSearcher();
// Search within a collection's index
$propertyFetcher = $totalcms->propertyFetcher();
// Retrieve specific properties from objects
$objectSaver = $totalcms->objectSaver();
$objectSaver->saveObject('blog', [
'id' => 'my-new-post',
'title' => 'Hello World',
'body' => 'This is my first post.',
]);
$objectUpdater = $totalcms->objectUpdater();
$objectUpdater->updateObject('blog', 'my-new-post', [
'title' => 'Updated Title',
]);
$objectRemover = $totalcms->objectRemover();
$objectRemover->removeObject('blog', 'my-new-post');
$objectCloner = $totalcms->objectCloner();
$objectCloner->cloneObject('blog', 'my-post', 'my-post-copy');
$incrementer = $totalcms->propertyIncrementer();
$incrementer->incrementProperty('products', 'widget-pro', 'stock', 10);
$incrementer->decrementProperty('products', 'widget-pro', 'stock', 1);

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');
// 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',
]);

Run background jobs programmatically.

$jobRunner = $totalcms->jobRunner();
// 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');

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

Section titled “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);

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();

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']);
MethodDescription
new TotalCMS(bool $autoStartBuffer = true)Initialize Total CMS. In CLI mode, session and buffer are skipped automatically.
MethodReturnsDescription
collectionLister()CollectionListerList available collections
collectionFetcher()CollectionFetcherFetch collection metadata
indexReader()IndexReaderRead collection indexes
indexSearcher()IndexSearcherSearch within indexes
indexBuilder()IndexBuilderRebuild collection indexes
objectFetcher()ObjectFetcherFetch full objects
objectSaver()ObjectSaverCreate new objects
objectUpdater()ObjectUpdaterUpdate existing objects
objectRemover()ObjectRemoverDelete objects
objectCloner()ObjectClonerClone/duplicate objects
propertyFetcher()PropertyFetcherFetch object properties
propertyIncrementer()ObjectPropertyIncrementerIncrement/decrement numeric properties
deckItemSaver()DeckItemSaverAdd items to deck properties
deckItemUpdater()DeckItemUpdaterUpdate deck items
deckItemRemover()DeckItemRemoverRemove deck items
deckItemFetcher()DeckItemFetcherFetch specific deck items
schemaFetcher()SchemaFetcherFetch schema definitions
schemaLister()SchemaListerList available schemas
fileSaver()FileSaverSave files programmatically
imageSaver()ImageSaverSave images programmatically
mailer()EmailServiceSend emails via templates
jobRunner()JobRunnerRun background jobs
MethodReturnsDescription
createLogger(string $name, bool $console = false, ?Level $level = null)LoggerInterfaceCreate a named logger for custom scripts
MethodReturnsDescription
clearCache()arrayClear all caches
disableCache()voidDisable cache reads for current process
MethodReturnsDescription
restrictPageAccess(array|string $groups = [], string $collection = '')voidRestrict page to authenticated users/groups
isUserLoggedIn(string $collection = '')boolCheck if a user is logged in
userData()arrayGet the logged-in user’s data
noCacheIfAuthenticated(string $collection = '')voidDisable browser caching for logged-in users
MethodReturnsDescription
startBuffer()voidStart output buffering
endBuffer()voidEnd output buffering
processBufferMacros(array $data = [], bool $restartBuffer = false)stringProcess buffered content through Twig
processMacros(string $templateName, array $data = [])stringRender a named Twig template
MethodReturnsDescription
sitemapForCollection(string $collection, array $options = [])stringGenerate sitemap XML
createSitemapResponse(string $collection, array $options = [])ResponseInterfaceCreate a PSR-7 sitemap response
MethodReturnsDescription
filePath(string $id, array $options = [])?stringGet filesystem path for a file property
depotPath(string $id, string $filePath, array $options = [])?stringGet filesystem path for a depot file
MethodReturnsDescription
TotalCMS::isPreview()boolCheck if running in preview mode
PropertyTypeDescription
configConfigAccess to the Total CMS configuration object