The Controller Layer for Admin Pages: WordPress MVC Mastery
Learn how to implement a Controller layer for WordPress admin pages. Separate your UI and business logic to maintain a scalable, professional plugin architecture.
Previously in this course, we explored the Creating Admin Menus lesson. While that lesson showed you how to register menus using simple callback functions, dumping all your HTML and business logic into the main plugin file will quickly lead to a "spaghetti code" nightmare as your Knowledge Base project grows.
In this lesson, we will implement the Controller layer of our MVC architecture. By shifting admin logic into a dedicated class, we ensure our main plugin file stays slim, readable, and focused solely on bootstrapping our services.
Why use a Controller?
In the context of WordPress, a controller is a class responsible for handling requests from the admin dashboard and returning the appropriate response (usually a rendered view).
When you follow the Designing for MVC in WordPress approach, you stop treating your main plugin file as a dumping ground for logic. Instead, you treat it like an orchestrator. Your main plugin class initializes your controllers, and your controllers handle the "how" of your admin pages.
Creating the AdminController
To keep our architecture clean, create a new file: app/Controllers/AdminController.php. This class will contain the logic previously handled by anonymous functions or global procedural callbacks.
PHPnamespace KnowledgeBase\Controllers; class AdminController { #6A9955">/** * Renders the main dashboard page for the Knowledge Base. */ public function renderDashboard() { #6A9955">// In a real-world scenario, you might pass data from a Model here. echo '<div class="wrap"><h1>Knowledge Base Dashboard</h1><p>Welcome to your KB admin area.</p></div>'; } #6A9955">/** * Handles the submenu page rendering. */ public function renderSettings() { echo '<div class="wrap"><h1>KB Settings</h1><p>Configure your plugin here.</p></div>'; } }
Routing Requests to Controller Methods
Now that we have a controller, we need to wire it into the WordPress menu system. We do this inside our main plugin class, which we established in Defining the Plugin Core Class.
Instead of passing a simple function name to add_menu_page, we pass an array containing our controller instance and the method name: [$this->adminController, 'renderDashboard'].
PHPnamespace KnowledgeBase; use KnowledgeBase\Controllers\AdminController; class Plugin { protected $adminController; public function __construct() { $this->adminController = new AdminController(); add_action('admin_menu', [$this, 'registerAdminMenus']); } public function registerAdminMenus() { add_menu_page( 'Knowledge Base', 'Knowledge Base', 'manage_options', 'kb-dashboard', [$this->adminController, 'renderDashboard'], #6A9955">// Routing here 'dashicons-book' ); add_submenu_page( 'kb-dashboard', 'Settings', 'Settings', 'manage_options', 'kb-settings', [$this->adminController, 'renderSettings'] ); } }
Hands-on Exercise: Refactor Your Menu
- Create the
app/Controllers/AdminController.phpfile in your plugin directory. - Move your existing menu callback logic from your main plugin file into the
AdminControllermethods. - Update your
registerAdminMenusmethod in the main plugin class to use the array syntax ([$this->adminController, 'methodName']) to point to your new controller. - Refresh your WordPress dashboard to ensure the menus still render correctly.
Common Pitfalls
- Namespace Mismatches: Ensure your
AdminControllerhas the correctnamespaceand that your autoloader is correctly mapping theapp/directory. If you get a "callable not found" error, double-check your class loading. - Dependency Bloat: Do not put database queries directly in your
AdminController. If you need to fetch data, follow the Laravel Repository Pattern logic: keep the controller thin and delegate data retrieval to a Model or Service layer. - Hardcoding Views: Eventually, you should move the HTML strings out of the controller and into dedicated template files (we will cover this in later lessons). For now, keeping them in the controller is a step up from the main plugin file, but keep the HTML minimal.
Recap
By introducing a controller, we have successfully decoupled our menu registration logic from our UI rendering logic. Your main plugin class is now responsible only for "hooking" things up, while the AdminController owns the execution of admin-facing requests. This separation of concerns is the bedrock of maintainable WordPress development.
Up next: We will move beyond static menus and start interacting with the database by Registering Custom Post Types for our Knowledge Base articles.
Work with me

Custom WordPress Plugin Development
Custom WordPress & WooCommerce plugins built to standard โ by the developer behind a plugin with 5,000+ active installs and a SaaS with 10,000+ users.

FilamentPHP Admin Panel & Dashboard Development
A powerful admin panel for your Laravel app โ built with FilamentPHP so you can manage everything without touching the database.