ATA WordPress MVC Plugin Development Helper
Supercharge your WordPress plugin with MVC-S structure
ATA helps WordPress plugin developers keep growing codebases organised and maintainable using the MVC design pattern.
ATA Folder STRUCTURE
your-plugin-name/
├── app/
│ ├── apis/ # REST API endpoint classes
│ ├── controllers/ # Controller classes
│ ├── helpers/ # Global helper functions
│ ├── models/ # Data logic (database interaction)
│ │ └── structures/ # Optional: DTOs/data structure classes
│ ├── routes/ # API and custom URL definitions
│ ├── services/ # Business logic layer
│ └── views/ # View files (HTML/PHP templates)
│
├── base/ # Base classes your app classes extend
├── config/ # Plugin-wide configuration
├── constants/ # Custom constant definitions
├── inc/ # Bootstrapping and initialization files
├── vendor/ # Composer dependencies (includes the core ATA framework)
│
└── your-plugin-name.php # The main plugin entry file
The core principles of ATA are:
Structured by Default
ATA provides a pre-defined directory structure that logically separates different parts of your plugin.
Object-Oriented First
It encourages writing code in classes rather than procedural functions, improving code reusability and organization.
Clear Separation of Concerns
It implements a variation of the MVC pattern to ensure your logic, data, and presentation layers are kept separate.
Simplified Routing
ATA includes a built-in router that makes registering custom REST API endpoints or custom URLs simple and declarative.
Key Differences
What is the MVC-S structure used by ATA?
ATA promotes a variant of the Model-View-Controller (MVC) pattern, which we call MVC-S (Model-View-Controller-Services).
Controller (The Central Engine)
The Controller is the core of your plugin’s operation. It is responsible for handling incoming requests, registering all WordPress hooks and filters, and controlling the application’s flow. For simple plugins, the Controller can handle all logic and data access directly. In more complex applications, its role evolves to become an orchestrator: it calls a Service for business logic or a Model for data, and then passes the results to a View for rendering.
View (The Presentation Layer)
The View is an optional layer for generating HTML output. It receives data from the Controller and renders it. A View should contain minimal PHP logic, focusing only on displaying the data it is given.
Model (The Data Layer)
The Model’s only responsibility is direct interaction with the database. Inside a Model class, you can access the WordPress database object via $this->db
(e.g., $this->db->get_results(...)
). This layer is optional; if your plugin doesn’t interact with the database, you don’t need it.
Service (The Logic Layer)
The Service layer is an optional but recommended place for your plugin’s core business logic. When logic becomes too complex for a Controller, you should extract it into a Service. This keeps Controllers thin and focused on handling requests.
The polylang-restapi-helper
plugin is a perfect, focused example of the ATA framework.
How to ATA MVC Your WordPress Plugin?
Documentation
How to Start Using ATA in Your Plugin—Fast and Easy
The Plugin Lifecycle & Main Controller
Every ATA plugin is managed by a central, root Controller. This main controller is the entry point for all plugin functionality and is responsible for managing the plugin’s lifecycle.
1. The Main Plugin File (your-plugin-name.php
) This file is very minimal. Its only jobs are to define essential constants, load the ATA framework, and instantiate your main controller.
Copied!// in your-plugin-name.php namespace YourPluginNamespace; // ... define constants ... // Load the framework and all its dependencies require_once PLUGIN_PATH . '/inc/main.php'; // Instantiate the one and only root controller $my_plugin = new MainController(); // Hook into WordPress's activation/deactivation events register_activation_hook(PLUGIN_FILE, [$my_plugin, 'activate']); register_deactivation_hook(PLUGIN_FILE, [$my_plugin, 'deactivate']);
2. The Main Controller (app/controllers/main-controller.php
) This controller contains the activation/deactivation logic and, most importantly, it loads all other controllers. This is where you can conditionally load features based on whether other plugins are active, saving memory and preventing errors.
Copied!// in app/controllers/main-controller.php namespace YourPluginNamespace; class MainController extends Controller { public function __construct() { parent::__construct(); $this->load_features(); } // Load other controllers for specific features private function load_features() { // Always load the admin UI and asset controllers (new AdminUiController())->main(); (new AssetsController())->main(); // **Conditionally load features** // Only load the WooCommerce compatibility layer if WooCommerce is active if (is_plugin_active('woocommerce/woocommerce.php')) { (new WooCommerceController())->main(); } } // Logic for when the plugin is activated (e.g., create DB tables) public function activate() { // ... your activation code ... } // Logic for when the plugin is deactivated (e.g., remove DB tables) public function deactivate() { // ... your deactivation code ... } }
Adding a page or tab to the WordPress Admin Panel
ATA provides a fluent interface to easily create admin pages with multiple tabs. This is managed from a controller.
Copied!// in app/controllers/admin-ui-controller.php namespace YourPluginNamespace; class AdminUiController extends Controller { public function main() { $this->add_wp_admin_pages(); } private function add_wp_admin_pages() { // Create a top-level page titled "My Plugin" $this->admin_page('My Plugin') // Only users who can 'manage_options' can see it ->permission('manage_options') // Add a "Dashboard" tab (calls the 'dashboard_page' method) ->add_tab('Dashboard', 'dashboard_page') // Add a "Settings" tab (calls the 'settings_page' method) ->add_tab('Settings', 'settings_page') // Register the page and all its tabs ->register(); } // This method is called when the 'Dashboard' tab is active public function dashboard_page() { $this->view = 'dashboard'; // Renders views/dashboard-view.php } // This method is called when the 'Settings' tab is active public function settings_page() { $this->view = 'settings'; // Renders views/settings-view.php } }
Hooking to a WordPress hook (Action)
ATA provides a convenient on()
helper method in its base controller to register action hooks. The constructor or a main()
method in your controller is the ideal place to register all hooks.
Copied!// in app/controllers/footer-message-controller.php namespace YourPluginNamespace; class FooterMessageController extends Controller { public function main() { // Hook into 'wp_footer' to call the 'add_message' method $this->on('wp_footer', 'add_message'); } public function add_message() { echo ""; } }
Using WordPress Filters
Similar to actions, you can use the filter()
helper method to modify data.
Copied!// in app/controllers/content-controller.php namespace YourPluginNamespace; class ContentController extends Controller { public function main() { // Hook into 'the_content' to modify post content $this->filter('the_content', 'add_author_bio'); } public function add_author_bio($content) { if (is_single() && in_the_loop() && is_main_query()) { $author_bio = '
'; return $content . $author_bio; } return $content; } }
Adding a WordPress REST API endpoint
Step 1: Register the Route in app/routes/main-routes.php
. This tells WordPress that your URL exists and which method should handle it.
Copied!// in app/routes/main-routes.php namespace YourPluginNamespace; $ata_router->api_version('my-plugin/v1'); $ata_router->api_method('GET')->api('/products')->call('ProductsApi::get_all_products');
Step 2: Create an API Class in app/apis/
to contain the logic for the endpoint.
Copied!// in app/apis/products-api.php namespace YourPluginNamespace; class ProductsApi extends Api { public function get_all_products() { // ... logic to get products ... return $this->send_success('Products retrieved.', ['products' => '...']); } }
How does routing work?
The route file (app/routes/main-routes.php
) is a powerful but optional feature. Its only job is to act as a switchboard, telling WordPress how to handle certain custom URLs. If your plugin doesn’t need custom URLs or a REST API, you don’t need to use it.
-
For REST APIs: Use the
api()
method. This registers a new endpoint under/wp-json/
.// Registers: example.com/wp-json/my-plugin/v1/orders $ata_router->api_version('my-plugin/v1'); $ata_router->api('/orders')->call('MyOrdersApi::get_orders');
-
For Custom Frontend URLs: Use the
page()
method. This creates a virtual page without needing a WordPress Page post.// Registers: example.com/my-custom-url/ $ata_router->page('/my-custom-url')->call('MyPageController::show_page');
What are the optional core directories for?
app/views
This directory holds your HTML templates. It is only necessary if your plugin generates a user interface, such as an admin settings page or a custom frontend page. Plugins that only provide background functionality or a REST API will likely not have a views
directory.
models/structures
(DTOs)
This directory is for Data Transfer Objects (DTOs). A DTO is a simple class used to define the shape of data being passed around. This directory is optional and may not be necessary for many plugins. It is most useful when you need to enforce a strict data structure, especially when communicating with an external API.