Skip to content

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:

  1. Feature Controllers - Classes that manage a specific feature
  2. Hook Managers - Classes that register WordPress hooks
  3. Service Classes - Shared services across the application
  4. Admin Interfaces - Dashboard and settings pages
  5. Block Registration - Block editor functionality

Don't Use Singleton For:

  1. Data Models - Objects representing data (posts, users)
  2. Value Objects - Simple data containers
  3. Utility Classes - Static method collections
  4. 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
	}
}
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

  1. Protected Constructor: Always make constructor protected or private
  2. Initialize in Constructor: Set up hooks and initialization in constructor
  3. Public Methods for API: Expose necessary functionality via public methods
  4. Avoid Static Data: Use instance properties instead of static properties
  5. Document Usage: Clearly document that class uses singleton pattern
  6. Single Responsibility: Each singleton should have one clear purpose
  7. Lazy Loading: Initialize resources only when needed
  8. 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.

Released under the MIT License.