The classic MVC pattern for WordPress development. Click each component to explore its role and real-world implementation.
Click a component ยท hover lines to explore
Deep Dive
The traffic director โ receives requests, coordinates Model and View.
The Controller is the entry point for every user request. It receives input, validates it, calls the appropriate Model methods to fetch or modify data, then passes that data to the View for rendering. In WordPress, Controllers appear as hook callbacks, REST API handlers, and AJAX processors.
WordPress action hooks act as Controllers โ they respond to events and orchestrate flow
// Controller via WordPress hook
add_action('init', function() {
if (!isset($_POST['submit_product'])) return;
// Validate
if (!wp_verify_nonce($_POST['_wpnonce'], 'add_product')) {
wp_die('Security check failed');
}
$name = sanitize_text_field($_POST['name']);
// Delegate to Model
$id = create_product(['name' => $name]);
// Route to View
wp_redirect(get_permalink($id));
exit;
});
register_rest_route() callbacks are the clearest Controller pattern in WordPress
// Controller โ REST endpoint
register_rest_route('myplugin/v1', '/products', [
'methods' => 'GET',
'callback' => function(WP_REST_Request $req) {
if (!current_user_can('read')) {
return new WP_Error('forbidden', 'Access denied',['status' => 403]);
}
$products = get_all_products($req->get_param('category'));
return rest_ensure_response($products);
},
'permission_callback' => '__return_true',
]);
wp_ajax_ hooks process async requests โ a perfect Controller responsibility
// Controller โ AJAX
add_action('wp_ajax_save_preference', function() {
check_ajax_referer('pref_nonce', 'nonce');
$key = sanitize_key($_POST['key']);
$value = sanitize_text_field($_POST['value']);
// Delegate to Model
$ok = update_user_preference(get_current_user_id(), $key, $value);
wp_send_json_success(['saved' => $ok]);
});
Deep DiveDeep Dive
The data layer โ owns data, enforces rules, never renders HTML.The traffic director โ receives requests, coordinates Model and View.
The Model manages all data persistence, business rules, and database interactions. It returns clean data structures to the Controller โ no HTML, no knowledge of how data will be displayed. In WordPress, Models live in Custom Post Types, WPDB queries, the Options API, and encapsulated data-access classes.
CPTs define your data schema โ the most fundamental Model concept in WordPress
// Model โ data schema
function register_product_cpt() {
register_post_type('product', [
'public' => true,
'label' => 'Products',
'supports' => ['title', 'editor', 'thumbnail'],
'show_in_rest' => true,
]);
register_post_meta('product', 'price', [
'type' => 'number',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'floatval',
]);
}
add_action('init', 'register_product_cpt');
Encapsulated queries โ the Model is the only layer that touches $wpdb
// Model โ data access function
function get_products_by_category(string $cat): array {
global $wpdb;
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID, post_title FROM {$wpdb->posts}
WHERE post_type = 'product'
AND post_status = 'publish'
AND post_excerpt = %s",
$cat
), ARRAY_A
);
return array_map(fn($r) => [
'id' => (int) $r['ID'],
'title' => $r['post_title'],
], $rows ?? []);
}
WordPress Options API is a ready-made Model for storing app configuration
// Model โ settings class
class PluginSettings {
private const KEY = 'myplugin_settings';
public static function get(string $key, $default = null) {
return (get_option(self::KEY, []))[$key] ?? $default;
}
public static function update(string $key, $value): bool {
$opts = get_option(self::KEY, []);
$opts[$key] = $value;
return update_option(self::KEY, $opts);
}
}
Deep Dive
The presentation layer โ renders data, never owns it.
The View renders the HTML output the user sees. It receives prepared data from the Controller and displays it โ without any business logic or database calls. In WordPress, Views are your template files, Gutenberg block save functions, shortcodes, and widget outputs.
single.php, archive.php, page.php are the View layer of WordPress themes
// View โ single.php
get_header();
$product = get_query_var('product'); // set by Controller
?>
<article class="product">
<h1><?php echo esc_html($product['title']); ?></h1>
<p class="price"><?php echo esc_html($product['price']); ?></p>
<div><?php echo wp_kses_post($product['description']); ?></div>
</article>
<?php get_footer();
add_shortcode() callback renders data โ it is a View, not a Controller
// View โ shortcode renderer
function render_product_card($atts) {
$data = get_query_var('product_' . $atts['id']);
ob_start(); ?>
<div class="product-card">
<h3><?php echo esc_html($data['title']); ?></h3>
<span><?php echo esc_html($data['price']); ?></span>
</div>
<?php return ob_get_clean();
}
add_shortcode('product_card', 'render_product_card');
The save() function is a pure View โ it outputs HTML from block attributes
// View โ Gutenberg block save()
export default function save({ attributes }) {
const { title, description, imageUrl } = attributes;
return (
<div className="wp-block-product-card">
<img src={imageUrl} alt={title} />
<h2>{title}</h2>
<p>{description}</p>
</div>
);
}