Singleton Pattern in Wikit
Overview
The Singleton pattern is extensively used throughout Wikit to ensure only one instance of a class exists. This is implemented via the SingletonTrait and is critical for proper WordPress hook management and resource efficiency.
SingletonTrait Implementation
Core Trait
php
namespace WDG\Core;
trait SingletonTrait {
/**
* Get the instance of the called class
*
* @param mixed $arg[]
* @return static
*/
public static function instance() {
static $instance;
if ( ! isset( $instance ) ) {
$instance = new static( ...func_get_args() );
}
return $instance;
}
/**
* Alias for instance
*
* @return static
*/
public static function get_instance() {
return static::instance( ...func_get_args() );
}
}When to Use Singleton
Use Singleton For:
- Feature Controllers - Classes that manage a specific feature
- Hook Managers - Classes that register WordPress hooks
- Service Classes - Shared services across the application
- Admin Interfaces - Dashboard and settings pages
- Block Registration - Block editor functionality
Don't Use Singleton For:
- Data Models - Objects representing data (posts, users)
- Value Objects - Simple data containers
- Utility Classes - Static method collections
- PostType/Taxonomy - These have their own instantiation pattern
Implementation Examples
Basic Feature Class
php
<?php
namespace WDG\Theme;
use WDG\Core\SingletonTrait;
class Navigation {
use SingletonTrait;
/**
* Constructor - MUST be protected or private
*/
protected function __construct() {
$this->init_hooks();
}
/**
* Initialize hooks
*/
private function init_hooks(): void {
add_action( 'init', [ $this, 'register_menus' ] );
add_filter( 'nav_menu_css_class', [ $this, 'add_menu_classes' ], 10, 2 );
}
/**
* Register navigation menus
*/
public function register_menus(): void {
register_nav_menus( [
'primary' => __( 'Primary Menu', 'wikit' ),
'secondary' => __( 'Secondary Menu', 'wikit' ),
] );
}
/**
* Add custom menu classes
*/
public function add_menu_classes( array $classes, $item ): array {
$classes[] = 'nav-item';
return $classes;
}
}Initialization in functions.php
php
// wikit-theme/functions.php
namespace WDG\Theme;
// Initialize singleton features
Navigation::instance();
BlockEditor::instance();
Breadcrumb::instance();
Footer::instance();
Share::instance();
// Or with parameters
CustomFeature::instance( $config_array );Advanced Implementation with Dependencies
php
<?php
namespace WDG\App;
use WDG\Core\SingletonTrait;
class AdvancedFeature {
use SingletonTrait;
/**
* Dependencies
*/
private ServiceClass $service;
private array $config;
/**
* Protected constructor with parameters
*/
protected function __construct( array $config = [] ) {
$this->config = wp_parse_args( $config, $this->get_defaults() );
$this->service = new ServiceClass();
$this->init();
}
/**
* Get default configuration
*/
private function get_defaults(): array {
return [
'enabled' => true,
'priority' => 10,
];
}
/**
* Initialize feature
*/
private function init(): void {
if ( ! $this->config['enabled'] ) {
return;
}
add_action( 'init', [ $this, 'setup' ], $this->config['priority'] );
}
/**
* Public method accessible via instance
*/
public function get_data(): array {
return $this->service->fetch_data();
}
}
// Usage
$feature = AdvancedFeature::instance( [ 'priority' => 5 ] );
$data = $feature->get_data();Common Patterns
Conditional Initialization
php
class ConditionalFeature {
use SingletonTrait;
protected function __construct() {
// Only initialize on frontend
if ( ! is_admin() ) {
$this->init_frontend();
}
// Only for logged-in users
if ( is_user_logged_in() ) {
$this->init_user_features();
}
}
}Late Initialization
php
class LateFeature {
use SingletonTrait;
private bool $initialized = false;
protected function __construct() {
// Delay initialization until needed
add_action( 'wp', [ $this, 'maybe_init' ] );
}
public function maybe_init(): void {
if ( $this->initialized ) {
return;
}
if ( $this->should_initialize() ) {
$this->initialize();
$this->initialized = true;
}
}
private function should_initialize(): bool {
return is_singular( 'product' );
}
}Accessing Instance from Other Classes
php
class OtherClass {
public function do_something(): void {
// Get singleton instance
$navigation = Navigation::instance();
// Access public methods
$navigation->register_menus();
// Or chain methods
BlockEditor::instance()->register_blocks();
}
}Real Wikit Examples
BlockEditor (wikit-theme)
php
namespace WDG\Theme;
class BlockEditor {
use \WDG\Core\SingletonTrait;
protected array $block_categories = [ /* ... */ ];
protected function __construct() {
add_filter( 'block_categories_all', [ $this, 'block_categories_all' ] );
add_action( 'init', [ $this, 'register_blocks' ] );
add_action( 'init', [ $this, 'register_pattern_categories' ] );
add_action( 'after_setup_theme', [ $this, 'theme_support' ] );
}
// Public methods for block management
public function register_blocks(): void {
// Implementation
}
}Breadcrumb (wikit-core)
php
namespace WDG\Core;
class Breadcrumb {
use SingletonTrait;
protected static bool $show_home = true;
protected function __construct() {
add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
add_filter( 'wdg/block-editor/config', [ $this, 'wdg_block_editor_config' ] );
}
// Public API for breadcrumb generation
public function get_breadcrumbs(): array {
// Implementation
}
}Testing Singleton Classes
Unit Testing
php
class SingletonTest extends WP_UnitTestCase {
public function test_singleton_returns_same_instance() {
$instance1 = TestClass::instance();
$instance2 = TestClass::instance();
$this->assertSame( $instance1, $instance2 );
}
public function test_singleton_with_parameters() {
$instance = ParameterizedClass::instance( [ 'key' => 'value' ] );
$this->assertInstanceOf( ParameterizedClass::class, $instance );
}
public function tearDown(): void {
// Reset singleton instances if needed
$this->reset_singletons();
parent::tearDown();
}
}Anti-Patterns to Avoid
DON'T: Public Constructor
php
// WRONG: Public constructor allows multiple instances
class WrongSingleton {
use SingletonTrait;
public function __construct() { // Should be protected/private
// This breaks the singleton pattern
}
}DON'T: Static Properties for Data
php
// WRONG: Shared static data can cause issues
class ProblematicSingleton {
use SingletonTrait;
private static array $data = []; // Avoid static data properties
// Better: Use instance properties
private array $instance_data = [];
}DON'T: Direct Instantiation
php
// WRONG: Never use 'new' with singleton classes
$instance = new Navigation(); // Will cause error if constructor is protected
// CORRECT: Always use instance()
$instance = Navigation::instance();Best Practices
- Protected Constructor: Always make constructor protected or private
- Initialize in Constructor: Set up hooks and initialization in constructor
- Public Methods for API: Expose necessary functionality via public methods
- Avoid Static Data: Use instance properties instead of static properties
- Document Usage: Clearly document that class uses singleton pattern
- Single Responsibility: Each singleton should have one clear purpose
- Lazy Loading: Initialize resources only when needed
- Clean Dependencies: Inject dependencies through instance() if needed
Performance Considerations
- Singleton pattern ensures hooks are registered only once
- Reduces memory usage by preventing duplicate instances
- Lazy initialization improves initial load time
- Static instance storage provides O(1) access time
Migration Guide
Converting Regular Class to Singleton
php
// Before
class RegularClass {
public function __construct() {
add_action( 'init', [ $this, 'init' ] );
}
}
$instance = new RegularClass();
// After
class SingletonClass {
use SingletonTrait;
protected function __construct() {
add_action( 'init', [ $this, 'init' ] );
}
}
SingletonClass::instance();The Singleton pattern is fundamental to Wikit's architecture and ensures efficient, organized code with proper hook management.