Controllers

Introduction

Controllers are a fundamental architectural component that provide a structured way to organize logic within the codebase. They follow object-oriented principles, encapsulating related functionality into cohesive classes that are easy to maintain, test, and extend. Controllers handle specific aspects of plugin functionality, separating business logic from presentation and persistence layers.

This documentation explores how to create and use controllers effectively, ensuring your code integrates seamlessly with the existing architecture while following established best practices.

Top ↑

Core Architecture

Top ↑

Controller Abstract Class

The foundation of the controller architecture is the Controller abstract class, which provides the structure and common functionality that all controllers inherit:

namespace TEC\Common\Contracts\Provider;

abstract class Controller {
	/**
	 * Whether the controller's hooks have been registered or not.
	 *
	 * @since TBD
	 *
	 * @var bool
	 */
	protected $registered = false;

	/**
	 * The DI container instance.
	 *
	 * @since TBD
	 *
	 * @var \TEC\Common\lucatume\DI52\Container
	 */
	protected $container;

	/**
	 * Constructor.
	 *
	 * @since TBD
	 *
	 * @param \TEC\Common\lucatume\DI52\Container $container The DI container instance.
	 */
	public function __construct( \TEC\Common\lucatume\DI52\Container $container ) {
		$this->container = $container;
	}

	/**
	 * Registers the controller's hooks with WordPress.
	 *
	 * @since TBD
	 */
	public function register() {
		if ( $this->registered ) {
			return;
		}

		$this->registered = true;
		$this->do_register();
	}

	/**
	 * Unregisters the controller's hooks with WordPress.
	 *
	 * @since TBD
	 *
	 * @return void Filters and actions hooks added by the controller are be removed.
	 */
	abstract public function unregister(): void;

	/**
	 * Registers the controller hooks.
	 *
	 * @since TBD
	 *
	 * @return void
	 */
	abstract protected function do_register(): void;
}

Top ↑

Registration Process

Controllers are typically registered through the Container/Service Provider system, which automatically handles instantiation and hook registration:

// In your plugin's service provider
namespace TEC\Events\Provider;

use TEC\Common\lucatume\DI52\ServiceProvider;
use TEC\Events\Controllers\Feature_Controller;

class Plugin_Provider extends ServiceProvider {
	/**
	 * Registers the controllers with the container.
	 *
	 * @since TBD
	 */
	public function register() {
		// Register controllers as singletons
		$this->container->singleton( Feature_Controller::class );
		
		// Register controllers with action-based loading
		$this->container->register_on_action( 'init', Feature_Controller::class );
		
		// Register controllers only when specific features are active
		$this->container->register_on_action( 
			'tec_events_custom_tables_v1_fully_activated', 
			\TEC\Events\Custom_Tables\Controllers\Event_Controller::class 
		);
		
		// Register controllers with aliases for easier access
		$this->container->bind( 'events.features', Feature_Controller::class );
	}
}

Top ↑

Why Use Controllers

Controllers provide several benefits over other architectural patterns:

  1. Separation of Concerns – Controllers isolate specific functionality, making code more maintainable and testable
  2. Reusability – Controller methods can be reused across different parts of the application
  3. Single Responsibility – Each controller handles a specific aspect of functionality
  4. Organization – Controllers provide a structured approach to organizing code
  5. Testability – Controllers can be easily mocked and tested in isolation

Top ↑

Controllers vs. Providers

While both Controllers and Providers are important components, they serve different purposes:

ControllersProviders
Handle business logicConfigure dependency injection
Register hooks with WordPressRegister services with the container
Respond to eventsBootstrap application components
Process and transform dataBind interfaces to implementations
Focus on behaviorFocus on structure

Providers are responsible for configuring the dependency injection container and bootstrapping application components. They define what classes should be instantiated and how they should be wired together, but they shouldn’t contain business logic.

Controllers, on the other hand, contain the actual business logic of your application. They respond to events, process data, and coordinate the flow of information between different parts of the system.

Using Controllers for your business logic instead of putting it directly in Providers leads to:

  1. Cleaner separation of concerns – Providers focus solely on bootstrapping, while Controllers handle functionality
  2. Easier testing – Controllers can be tested in isolation without the complexity of dependency injection
  3. More flexible architecture – Controllers can be selectively loaded based on conditions
  4. Better organization – Related functionality is grouped together in cohesive classes

Top ↑

Creating Custom Controllers

Top ↑

Basic Implementation

To create a custom controller:

  1. Create a class that extends Controller
  2. Implement the required method do_register()
  3. Optionally implement do_unregister() if cleanup is needed
  4. Register your controller with the container
namespace TEC\Events\Controllers;

use TEC\Common\Contracts\Provider\Controller as Controller_Contract;

class Feature_Controller extends Controller_Contract {
	/**
	 * Determines if this controller will register.
	 * This is present due to how UOPZ works, it will fail if method belongs to the parent/abstract class.
	 *
	 * @since TBD
	 *
	 * @return bool Whether the controller is active or not.
	 */
	public function is_active(): bool {
		return true;
	}

	/**
	 * Register the controller hooks.
	 *
	 * @since TBD
	 *
	 * @return void
	 */
	protected function do_register(): void {
		$this->add_actions();
		$this->add_filters();
	}

	/**
	 * Unregister the controller hooks.
	 *
	 * @since TBD
	 *
	 * @return void
	 */
	public function unregister(): void {
		$this->remove_actions();
		$this->remove_filters();
	}

	/**
	 * Add actions for the controller.
	 *
	 * @since TBD
	 *
	 * @return void
	 */
	protected function add_actions(): void {
		add_action( 'init', [ $this, 'initialize' ] );
		add_action( 'admin_init', [ $this, 'admin_initialize' ] );
	}

	/**
	 * Remove actions for the controller.
	 *
	 * @since TBD
	 *
	 * @return void
	 */
	protected function remove_actions(): void {
		remove_action( 'init', [ $this, 'initialize' ] );
		remove_action( 'admin_init', [ $this, 'admin_initialize' ] );
	}

	/**
	 * Add filters for the controller.
	 *
	 * @since TBD
	 *
	 * @return void
	 */
	protected function add_filters(): void {
		add_filter( 'some_filter', [ $this, 'filter_callback' ] );
	}

	/**
	 * Remove filters for the controller.
	 *
	 * @since TBD
	 *
	 * @return void
	 */
	protected function remove_filters(): void {
		remove_filter( 'some_filter', [ $this, 'filter_callback' ] );
	}

	/**
	 * Initialize the controller.
	 *
	 * @since TBD
	 *
	 * @return void
	 */
	public function initialize(): void {
		// Initialize your feature here
	}

	/**
	 * Initialize admin-specific functionality.
	 *
	 * @since TBD
	 *
	 * @return void
	 */
	public function admin_initialize(): void {
		// Initialize admin functionality
	}

	/**
	 * Filter callback example.
	 *
	 * @since TBD
	 *
	 * @param mixed $value The value to filter.
	 *
	 * @return mixed The filtered value.
	 */
	public function filter_callback( $value ) {
		// Filter logic here
		return $value;
	}
}

Top ↑

Conditional Registration with Container’s register_on_action

For performance optimization, the container provides a register_on_action method that allows you to register controllers only when specific actions have been triggered:

// Example of container register_on_action usage in a provider
namespace TEC\Events\Provider;

class Plugin_Provider extends \TEC\Common\lucatume\DI52\ServiceProvider {
	/**
	 * Registers the providers and controllers.
	 *
	 * @since TBD
	 */
	public function register() {
		// Always register the CT1 migration component of Flexible Tickets.
		$this->container->register_on_action( 'tec_pro_ct1_provider_registered', \TEC\Tickets\Flexible_Tickets\Series_Passes\CT1_Migration::class );
		
		// Upon CT1 full activation register the rest of the components.
		$this->container->register_on_action(
			'tec_events_pro_custom_tables_v1_fully_activated',
			\TEC\Tickets\Flexible_Tickets\Provider::class
		);
	}
}

This approach prevents unnecessary loading of controllers when they aren’t needed for the current request, improving performance. The container handles the registration of the controller only when the specified action is fired.

Here’s how it works:

  • If the action has already been fired, the controller is registered immediately
  • If the action hasn’t been fired yet, the container adds a hook to register the controller when the action occurs
  • The controller is only registered once, even if the action fires multiple times

Top ↑

Working with Dependencies

Controllers can have dependencies injected through their constructor:

namespace TEC\Tickets\Flexible_Tickets\Series_Passes;

use TEC\Common\Contracts\Provider\Controller;
use TEC\Common\lucatume\DI52\Container;

class Emails extends Controller {
	/**
	 * A reference to the Upcoming Series Events List instance.
	 *
	 * @since TBD
	 *
	 * @var Upcoming_Series_Events_List
	 */
	private Upcoming_Series_Events_List $upcoming_events_list;

	/**
	 * Emails constructor.
	 *
	 * @since TBD
	 *
	 * @param Container                   $container            The DI container.
	 * @param Upcoming_Series_Events_List $upcoming_events_list The Upcoming Series Events List instance.
	 */
	public function __construct( Container $container, Upcoming_Series_Events_List $upcoming_events_list ) {
		parent::__construct( $container );
		$this->upcoming_events_list = $upcoming_events_list;
	}

	/**
	 * {@inheritDoc}
	 *
	 * @since TBD
	 *
	 * @return void
	 */
	protected function do_register(): void {
		add_filter( 'tec_tickets_emails_registered_emails', [ $this, 'add_series_to_registered_email_types' ] );
		
		// Hook additional functionality only when needed
		$wallet_plus_email_controller_action = 'tec_container_registered_provider_' . Wallet_Plus_Email_Controller::class;
		if ( did_action( $wallet_plus_email_controller_action ) ) {
			$this->hook_wallet_plus_filters();
		} else {
			add_action( $wallet_plus_email_controller_action, [ $this, 'hook_wallet_plus_filters' ] );
		}
	}
	
	/**
	 * Hooks the Email filters provided by Wallet Plus.
	 *
	 * @since TBD
	 *
	 * @return void
	 */
	public function hook_wallet_plus_filters(): void {
		$wallet_plus_controller = $this->container->get( Wallet_Plus_Email_Controller::class );
		add_filter(
			'tec_tickets_emails_series-pass_settings',
			[ $wallet_plus_controller, 'add_ticket_email_settings' ]
		);
	}
}

The DI container automatically resolves these dependencies when instantiating the controller.

Top ↑

Handling Hooks

Controllers typically organize hooks in the do_register() method:

/**
 * Registers the controller hooks.
 *
 * @since TBD
 *
 * @return void
 */
protected function do_register(): void {
	// WordPress core hooks
	add_action( 'init', [ $this, 'initialize' ] );
	add_action( 'wp_loaded', [ $this, 'late_initialize' ] );
	
	// Admin-specific hooks
	if ( is_admin() ) {
		add_action( 'admin_init', [ $this, 'admin_initialize' ] );
		add_action( 'admin_menu', [ $this, 'register_menu_pages' ] );
	}
	
	// AJAX hooks
	add_action( 'wp_ajax_feature_action', [ $this, 'handle_ajax' ] );
	
	// Custom hooks
	add_action( 'plugin_custom_hook', [ $this, 'custom_hook_callback' ] );
	
	// Filters
	add_filter( 'plugin_filter', [ $this, 'filter_data' ], 10, 2 );
}

For more complex controllers, you can organize hooks into separate methods:

/**
 * Registers the controller hooks.
 *
 * @since TBD
 *
 * @return void
 */
protected function do_register(): void {
	$this->register_core_hooks();
	$this->register_admin_hooks();
	$this->register_ajax_hooks();
	$this->register_custom_hooks();
}

/**
 * Registers WordPress core hooks.
 *
 * @since TBD
 *
 * @return void
 */
private function register_core_hooks(): void {
	add_action( 'init', [ $this, 'initialize' ] );
	add_action( 'wp_loaded', [ $this, 'late_initialize' ] );
}

/**
 * Registers admin-specific hooks.
 *
 * @since TBD
 *
 * @return void
 */
private function register_admin_hooks(): void {
	if ( ! is_admin() ) {
		return;
	}
	
	add_action( 'admin_init', [ $this, 'admin_initialize' ] );
	add_action( 'admin_menu', [ $this, 'register_menu_pages' ] );
}

/**
 * Registers AJAX hooks.
 *
 * @since TBD
 *
 * @return void
 */
private function register_ajax_hooks(): void {
	add_action( 'wp_ajax_feature_action', [ $this, 'handle_ajax' ] );
	add_action( 'wp_ajax_nopriv_feature_action', [ $this, 'handle_public_ajax' ] );
}

/**
 * Registers custom hooks.
 *
 * @since TBD
 *
 * @return void
 */
private function register_custom_hooks(): void {
	add_action( 'plugin_custom_hook', [ $this, 'custom_hook_callback' ] );
	add_filter( 'plugin_filter', [ $this, 'filter_data' ], 10, 2 );
}

Top ↑

Best Practices

When working with controllers, follow these best practices:

1. Single Responsibility: Each controller should handle a specific aspect of functionality. If a controller grows too large, consider splitting it into multiple controllers.

2. Early Returns: Use early returns to handle edge cases and avoid deep nesting of conditions:

public function process_request( $request ) {
	// Bail if not a valid request
	if ( ! is_array( $request ) || empty( $request ) ) {
		return;
	}
	
	// Bail if missing required data
	if ( ! isset( $request['action'] ) ) {
		return;
	}
	
	// Process the request
	$this->handle_action( $request['action'] );
}

3. Dependency Injection: Use constructor injection for dependencies rather than global functions or static methods:

// Good: Dependencies injected through constructor
public function __construct( Container $container, Data_Helper $helper, Logger $logger ) {
	parent::__construct( $container );
	$this->helper = $helper;
	$this->logger = $logger;
}

// Avoid: Global functions or static methods
public function process() {
	global $wpdb;
	$results = $wpdb->get_results( "SELECT * FROM {$wpdb->posts}" );
	
	Static_Helper::process_results( $results );
}

4. Contextual Registration: Use the container’s register_on_action method to only load controllers when they’re needed:

// In your provider
$this->container->register_on_action( 
	'tec_events_custom_tables_v1_fully_activated', 
	\TEC\Events\Custom_Tables\Controllers\Event_Controller::class 
);

5. Unregistration: Implement do_unregister() to properly clean up when functionality is disabled:

protected function do_unregister(): void {
	remove_action( 'init', [ $this, 'initialize' ] );
	remove_filter( 'some_filter', [ $this, 'filter_callback' ] );
}

Top ↑

Troubleshooting

Top ↑

Controller Not Registering

If your controller isn’t registering properly:

1. Check Container Registration: Ensure your controller is properly registered with the container:

// In your service provider
public function register() {
	// Make sure this is called
	$this->container->singleton( \TEC\Events\Controllers\Feature_Controller::class );
}

2. Verify Hook Registration: Make sure your controller’s register() method is being called:

// Debug with Xdebug
public function register() {
	// Set a breakpoint here
	parent::register();
}

3. Check Action Timing: When using register_on_action, ensure the action actually fires:

// Check if the action is firing
add_action( 'tec_events_custom_tables_v1_fully_activated', function() {
    // Set a breakpoint here to confirm the action is firing
});

Top ↑

Hook Timing Issues

If your controller hooks are firing at the wrong time:

1. Check Hook Priority: Adjust the priority of your hook registrations:

// Use a lower priority to run earlier (default is 10)
add_action( 'init', [ $this, 'initialize' ], 5 );

// Use a higher priority to run later
add_action( 'init', [ $this, 'late_initialize' ], 20 );

2. Use Later Hooks: If a hook is firing too early, use a later hook in the WordPress lifecycle:

// Instead of 'init'
add_action( 'wp_loaded', [ $this, 'initialize' ] );

// Instead of 'admin_init'
add_action( 'admin_menu', [ $this, 'initialize_admin' ] );

3. Debug Hook Execution: Use Xdebug to track hook execution order:

public function initialize() {
	// Set an Xdebug breakpoint here to track execution timing
	// Actual initialization code
}

Top ↑

Dependency Issues

If your controller has dependency problems:

1. Check Constructor Signature: Ensure your constructor matches what the container expects:

// Make sure parameter names and types match what's registered in the container
public function __construct( \TEC\Common\lucatume\DI52\Container $container, Data_Helper $helper ) {
	parent::__construct( $container );
	$this->helper = $helper;
}

2. Register Dependencies: Make sure all dependencies are registered with the container:

// In your service provider
public function register() {
	// Register dependencies first
	$this->container->singleton( \TEC\Events\Helpers\Data_Helper::class );
	
	// Then register the controller
	$this->container->singleton( \TEC\Events\Controllers\Feature_Controller::class );
}

3. Use Optional Dependencies: For non-critical dependencies, make them optional:

public function __construct( \TEC\Common\lucatume\DI52\Container $container, Data_Helper $helper = null ) {
	parent::__construct( $container );
	$this->helper = $helper ?: new Data_Helper();
}

By following these guidelines and best practices, you can create well-structured, maintainable controllers that integrate seamlessly with the existing architecture.