Developer Guide
This guide is intended for developers who want to understand the internals, extend the engine, or contribute to CandidTemplate. It covers architecture, design decisions, class responsibilities, rendering pipeline, and roadmap.
---1. Core Philosophy
HTML is the source of truth. The engine manipulates real DOM — never string-replaces or rewrites templates.
- No string templating — no
{{ }}, no%placeholder%, no regex replacement - Real DOM — every operation works on a
DOMDocumenttree - Deferred binding —
pick()queues changes, applied only atrender() - Clean separation — structure in HTML, binding in PHP, no mixing
- Designer-friendly — HTML files are valid, openable in any browser without PHP
2. Core Classes
| Class | Role | Responsibility |
|---|---|---|
CandidTemplate |
Core Engine | DOM manipulation, slot/include/loop rendering, assignment application, data store, hook system, debug output. Self-sufficient — works without adaptor. |
CandidTemplateAdaptor |
File System Bridge |
File path resolution, body extraction, singleton access,
data file auto-loading. Thin layer over
CandidTemplate — no rendering logic.
|
CandidDomContext |
DOM Parser |
Wraps DOMDocument, protects
<script> and <style>
content, handles fragment vs full document detection,
Vue event attribute protection, serialization.
|
CandidElement |
Element API |
Fluent API for single element manipulation.
Reads pending assignments before DOM for accurate
chained operations. Wraps CandidTemplate
assignment methods.
|
CandidElements |
Collection API |
Iterable collection of CandidElement instances.
Supports bulk operations, first/last detection,
filtering, and counting.
|
CandidViewResolver |
Path Resolver | Resolves template file paths using hierarchical directory walking. Supports base path, app path, common path, and result caching. |
3. Class Responsibility Boundary
The most important boundary is between
CandidTemplate and CandidTemplateAdaptor:
| Concern | CandidTemplate | CandidTemplateAdaptor |
|---|---|---|
| DOM manipulation | ✅ | ❌ |
| Slot rendering | ✅ | ❌ |
| Loop rendering | ✅ | ❌ |
| Assignment application | ✅ | ❌ |
| Hook system (onLoad) | ✅ | ❌ |
| Data store (set/get/share) | ✅ | ❌ |
| File path resolution | ❌ | ✅ |
| Body extraction | ❌ | ✅ |
| Data file auto-loading | ❌ | ✅ |
| Singleton access | ❌ | ✅ |
4. Rendering Pipeline
CandidTemplate::render()
│
├── applyDataFiles()
│ └── execute each registered .data.php file
│ ($this = template instance)
│
├── renderIncludes() ← pass 1
│ └── resolve include selectors → replaceTargetWithHtml()
│
├── renderSlots()
│ └── for each registered slot:
│ └── slot->render() ← recursive
│ ├── applyDataFiles()
│ ├── renderIncludes()
│ ├── renderSlots()
│ ├── renderIncludes()
│ ├── applyLoops()
│ └── applyAssignments()
│ replaceTargetWithHtml() ← wrapper removed
│
├── renderIncludes() ← pass 2 (catches slot-introduced includes)
│
├── applyLoops()
│ └── for each loop container:
│ ├── remove original container
│ └── for each item:
│ ├── renderIncludes()
│ ├── renderSlots()
│ └── applyAssignments()
│
├── applyAssignments()
│ └── apply all pick() queued bindings to DOM
│
└── ctx->render()
└── serialize DOM → HTML string
restore Vue event attributes (@click etc.)
html_entity_decode
---
5. Slot Wrapper Removal
A key design decision — the data-slot wrapper element
is replaced by its content, not filled.
This keeps rendered HTML clean with no leftover placeholder divs.
// replaceTargetWithHtml() — used by both renderSlots() and renderIncludes()
private function replaceTargetWithHtml(DOMNode $target, CandidTemplate $tpl): void
{
$parent = $target->parentNode;
if (!$parent) return;
$html = $tpl->render();
foreach ($this->importHtml($html) as $n) {
$parent->insertBefore($n, $target);
}
$parent->removeChild($target);
}
---
6. Assignment System
All pick() calls are queued — not applied immediately.
The internal structure:
// Internal assignments map
$assignments = [
'selector' => [
0 => ['data' => 'plain string or array'], // index 0
1 => ['data' => [...]] // index 1 (pickAll)
]
];
// Array data structure (for content + attribute assignments)
[
'content' => 'Hello World',
'attributes' => ['class' => 'active', 'href' => '/home']
]
Priority during applyAssignments():
- If
remove: true— element removed from DOM - If array with
attributes— attributes applied first - If array with
content— inner HTML replaced - If plain string — element itself replaced via
replaceNode()
7. Selector Resolution
findNodes() converts selectors to XPath internally:
| Selector Type | Example | XPath Generated |
|---|---|---|
| Simple (legacy) | nav_home |
.//*[@id='nav_home'] | .//*[@name='nav_home'] | .//nav_home |
| ID | #nav_home |
.//*[@id='nav_home'] |
| Class | .nav-link |
.//*[contains(@class,'nav-link')] |
| Attribute | [data-id] |
.//*[@data-id] |
| Attribute value | [data-id=123] |
.//*[@data-id='123'] |
| Tag + Class | a.nav-link |
.//a[contains(@class,'nav-link')] |
div > a), pseudo-selectors (:first-child),
and tag+id (div#main) are not supported.
Use plain XPath via DOMXPath directly for advanced cases.
8. Data Store — Inheritance Model
root->share('appName', 'Sony News') ← stored on root
│
├── content template
│ get('appName') ✅ ← walks parent chain to root
│ ├── slider
│ │ get('appName') ✅ ← inherited through chain
│ └── sidebar
│ get('appName') ✅
└── footer
get('appName') ✅
root->set('theme', 'dark')
├── content → get('theme') ✅ ← parent data flows down
└── footer → get('theme') ✅
content->set('slides', 5)
root → get('slides') ❌ ← child data does not flow up
footer → get('slides') ❌
slider → get('slides') ✅ ← only child of content
---
9. Script and Style Protection
DOMDocument mangles content inside
<script> and <style> blocks.
CandidDomContext protects them using token substitution:
Load HTML
↓
Replace <script>/<style> content with ___CANDID_SCRIPT_N___ tokens
↓
DOMDocument::loadHTML()
↓
Restore original content from token map
↓
Safe serialization via saveHTML()
---
10. Vue Event Compatibility
Vue event bindings use @ prefix which DOMDocument
cannot parse. They are transparently protected and restored:
@click="handler"
↓ protectVueEvents()
data-candid-on-click="handler"
↓ DOMDocument parses safely
data-candid-on-click="handler"
↓ restoreVueEvents() in render()
@click="handler"
---
11. Key Design Decisions
| Decision | Reason | Alternative Considered |
|---|---|---|
| DOM-based, not string-based | Accurate, no regex edge cases | Regex/str_replace templating |
| Deferred assignments | Allows chaining, order independence | Immediate DOM mutation |
| Slot wrapper removal | Clean output, no leftover divs | Keep wrapper, clear contents |
| Two-pass include rendering | Supports includes inside slots | Single-pass only |
| data-slot attribute | Valid HTML, browser-safe | Custom tags, comments |
| Adaptor as thin file bridge | Core engine stays framework-agnostic | File loading inside CandidTemplate |
| Script/style token protection | Prevents DOMDocument mangling | Custom HTML parser |
12. Extension Points
Recommended ways to extend without modifying core:
- New selector strategies — extend
selectorToXpath()with additional CSS patterns - Custom utility functions — wrap
pick()chains into reusable helpers (e.g.updateImage(),updateLink()) - Custom adaptor — extend
CandidTemplateAdaptorfor different file structures or frameworks - Data file conventions — establish naming conventions for auto-resolved data files
- Hook system — use
onLoad()for cross-cutting concerns (asset URL rewriting, i18n replacement)
13. Error Handling Strategy
| Situation | Behaviour | Exception Type |
|---|---|---|
| View file not found | Throws immediately | RuntimeException |
| Invalid base path | Throws immediately | RuntimeException |
| Data file not found | Throws immediately | InvalidArgumentException |
| Forbidden superglobal in data file | Throws before execution | InvalidArgumentException |
| Duplicate loop container | Throws immediately | InvalidArgumentException |
| Loop container not found | Throws immediately | InvalidArgumentException |
| Selector not found in pick() | Silent — returns null element | None |
| Slot placeholder not found | Silent — slot skipped | None |
| render() on loop item | Throws immediately | LogicException |
14. Debugging
Both CandidTemplate and CandidElement
provide built-in debug output:
// Full template debug — shows assignments, slots, loops, data store
echo $tpl->debugHtml();
// Specific slot debug
echo $tpl->debugHtml('slider');
// Element debug — shows pending assignments for one element
echo $tpl->pick('#newsCard')->debugHtml();
Debug output includes:
- Template name, page, basePath, parent status
- All pending assignments with selector, content, attributes
- Registered slots and their assignment state
- Registered includes
- Loop containers and item count
- Shared data store values
- Rendered HTML preview (collapsible)
15. Coding Guidelines
- All files must declare
strict_types=1 - All methods must have full type hints including return types
- Keep methods small and single-purpose
- Core engine (
CandidTemplate) must remain framework-agnostic — no file I/O, no HTTP, no framework dependencies - Adaptor handles all file system concerns — never add file resolution to core
- Avoid side effects during rendering —
render()should be idempotent - New selector types belong in
selectorToXpath() - New element operations belong in
CandidElement - Maintain backward compatibility — existing selectors must continue to work
16. Testing Strategy
- Unit — test each method of
CandidElementin isolation - Integration — test full render pipeline with nested slots and loops
- Selector — test all selector types against known HTML fixtures
- Assignment — test deferred assignment application order and priority
- Data store — test parent chain inheritance
for
get()andshare() - Security — test data file validation rejects all forbidden superglobals
- Vue compatibility — test event attribute protection and restoration round-trip
- Script/style protection — test token substitution preserves content byte-for-byte
17. Performance Considerations
DOMDocumentparsing is the largest cost — one parse per template file per requestgetWrapperElement()caches its XPath result after first callCandidViewResolvercaches resolved file paths — repeated slot calls for the same file hit cache- Loop items clone DOM subtrees — keep loop container HTML lean
getMergedAssignments()walks the parent chain iteratively — keep template nesting shallow- Use
slotIf(false)to skip file I/O entirely for conditional sections
18. Planned Features — Roadmap
| Feature | Description | Priority |
|---|---|---|
| Output caching | Cache rendered slot output with TTL | 🟢 High |
| XSS escaping | safeContent() — auto-escape user input |
🟢 High |
| Partial render | renderSlot('content') for AJAX responses |
🟡 Medium |
| Asset versioning | Auto append ?v=hash to asset URLs |
🟡 Medium |
| Meta/head injection | setMeta(), addScript(),
addStyle() |
🟡 Medium |
| i18n support | Auto-replace data-i18n keys
from language files |
🟡 Medium |
| Component system | data-component attribute —
semantic reusable includes |
🔵 Low |
| Template inheritance | Blade-style extends() /
section() |
🔵 Low |
| Event system | on('beforeRender'),
on('afterRender') |
🔵 Low |
19. Versioning Strategy
| Version | Status | Commitment |
|---|---|---|
v0.x |
Experimental | API may change without notice |
v1.0 |
Stable | No breaking changes within major version |
v1.x |
Active | Backward compatible features and fixes |
v2.0+ |
Future | Breaking changes with migration guide |
20. Contribution Guidelines
- Follow existing class responsibility boundaries — do not add file I/O to core
- Add
declare(strict_types=1)to all new files - Add full type hints to all new methods
- Write tests for all new features before submitting
- Update relevant documentation sections
- Do not break existing selector behaviour
- New element operations must handle
pending assignments — read via
getPendingAttribute()not raw DOM