VOOZH about

URL: https://deepwiki.com/calevans/staticforge/7.3-creating-custom-features

⇱ Creating Custom Features | calevans/staticforge | DeepWiki


Loading...
Last indexed: 11 February 2026 (5f6a2a)
Menu

Creating Custom Features

This page provides a comprehensive guide to building custom features for StaticForge. It covers the architecture, implementation patterns, and best practices for extending the system with your own functionality.

For information about the overall feature architecture and discovery mechanism, see Feature Architecture. For details on using built-in features, see Built-in Features Overview. For distributing features via Composer, see External Features.


Feature Creation Workflow

The following diagram illustrates the complete lifecycle of creating and registering a custom feature:

Diagram: Feature Development and Registration Lifecycle


Sources:


Basic Feature Structure

Directory and File Organization

Features reside in src/Features/ with the following structure:

src/Features/MyFeature/
├── Feature.php # Main feature class (required)
├── Services/ # Service classes (optional)
│ └── MyFeatureService.php
└── Models/ # Data models (optional)
 └── MyModel.php

The FeatureManager scans for directories containing a Feature.php file during bootstrap.

Sources:

Minimal Feature Implementation

Diagram: BaseFeature Class Hierarchy


Minimal Feature Example:


Sources:


Event Listener Registration

The eventListeners Array

The eventListeners property is a declarative mapping of events to handler methods. BaseFeature.register() automatically registers these listeners during feature initialization.

Array Structure:

KeyValue TypeDescription
Event name['method' => string, 'priority' => int]Event → Handler mapping

Example:


Sources:

Priority System

Events execute in ascending priority order (lower numbers first):

Priority RangeUse Case
0-50Early initialization, dependency setup
50-100Standard processing, data collection
100-200Data transformation, enrichment
200-500Final processing, cleanup
500+Late-stage modifications

Critical Priority Interactions:

POST_GLOB Event:
├─ CategoryIndex (50) - Creates category index pages
├─ MenuBuilder (100) - Builds menus (needs index pages to exist)
├─ Tags (150) - Processes tags
└─ Categories (250) - Applies category templates

Sources:

Event Handler Signature

All event handlers must follow this signature:


Key Rules:

  1. Accept Container and array parameters
  2. Return modified array (always)
  3. Never break the event chain by not returning data

Sources:


Accessing Dependencies

Container Access

The Container provides access to all registered services and configuration:

Diagram: Container Service Access Patterns


Common Container Methods:

MethodPurposeExample
get(string)Retrieve service instance$container->get('logger')
getVariable(string)Retrieve configuration value$container->getVariable('OUTPUT_DIR')
setVariable(string, mixed)Store data for other features$container->setVariable('my_data', $value)
add(string, object)Register service (rarely needed in features)$container->add('my_service', $instance)

Sources:

Logger Usage

Every feature has access to a shared logger instance:


Log Levels: DEBUG, INFO, WARNING, ERROR, CRITICAL

Sources:


Service-Oriented Architecture

When to Extract Services

Extract business logic into service classes when:

  1. Feature logic exceeds ~100 lines
  2. Logic is reusable across multiple event handlers
  3. Testing requires isolation from the event system
  4. Complex data transformations are needed

Example Service Structure:

src/Features/MyFeature/
├── Feature.php # Thin orchestration layer
└── Services/
 ├── MyFeatureService.php # Core business logic
 └── DataTransformService.php # Specialized transformations

Service Pattern Implementation

Feature.php (Orchestrator):


Services/MyFeatureService.php (Business Logic):


Sources:


Configuration Management

Implementing ConfigurableFeatureInterface

Features requiring configuration should implement ConfigurableFeatureInterface:


This enables validation via audit:config command.

Sources:

Reading Configuration

From siteconfig.yaml:


From Environment Variables:


Configuration Hierarchy:

siteconfig.yaml → BaseFeature.getConfig() → Returns site_config[feature_name]
.env → $_ENV or getenv()
Container Variables → container.getVariable()

Sources:


Advanced Patterns

Sharing Data with Templates

Features expose data to templates by storing it in the features parameter:


Template Access:


Sources:

Feature-Specific Events

Features can fire their own events for extensibility:


Example from RssFeed Feature:

The RssFeed feature fires RSS_BUILDER_INIT and RSS_ITEM_BUILDING events, allowing other features (like Podcast) to extend RSS functionality without modifying the core RSS feature.

Sources:

File Discovery Pattern

Access discovered files during POST_GLOB:


discovered_files Structure:


Sources:


Feature Priority and Ordering

Discovery Priority

Features are loaded in three tiers:

Diagram: Feature Discovery Priority Hierarchy


Features with identical names are resolved by priority: User > Composer > Library.

Sources:

Disabling Features

Disable features via siteconfig.yaml:


Disabled features:

  • Are not instantiated
  • Do not register event listeners
  • Do not consume resources

Sources:


Testing Custom Features

Unit Test Structure

Create unit tests extending UnitTestCase:


Sources:


Distribution via Composer

Package Structure

To distribute a feature as a Composer package:

my-vendor/my-feature/
├── composer.json
└── src/
 └── Features/
 └── MyFeature/
 ├── Feature.php
 └── Services/
 └── MyService.php

composer.json Configuration


The extra.staticforge.feature key registers the feature class for automatic discovery.

Sources:


Complete Example: Reading Time Feature

This example demonstrates a complete feature that calculates reading time for content:

src/Features/ReadingTime/Feature.php:


Template usage:


Sources:


Summary

Custom features in StaticForge follow these principles:

  1. Extend BaseFeature for automatic event registration and container access
  2. Use the eventListeners array for declarative event binding
  3. Follow priority conventions to ensure correct execution order
  4. Extract services for complex business logic
  5. Implement ConfigurableFeatureInterface for features requiring configuration
  6. Return parameters from all event handlers (bucket brigade pattern)
  7. Store in src/Features/ for automatic discovery
  8. Test in isolation using UnitTestCase

Features have full access to the container, discovered files, configuration, and can fire their own events for extensibility.

Sources: