Model ยท View ยท Controller

The classic MVC pattern for WordPress development. Click each component to explore its role and real-world implementation.

Controller Model View

Click a component ยท hover lines to explore

  • Receive and validate user input
  • Invoke Model to read or write data
  • Choose which View to render
  • Handle authentication & authorization
  • Stay thin โ€” no SQL, no HTML
Hook Callbacks (init, save_post)
// 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;
});
REST API Route Handler
// 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',
]);
AJAX Handler
// 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]);
});
  • Define data structures and schemas (CPTs, Taxonomies)
  • Handle all CRUD operations (WPDB, WP APIs)
  • Enforce data validation and business rules
  • Manage caching and query optimization
  • Return clean typed data โ€” never HTML
Custom Post Type (Schema)
// 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');
Data Access Layer (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 ?? []);
}
Options API / Settings
// 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);
    }
}
  • Render HTML output from structured data
  • Format and localize values (dates, prices, labels)
  • Manage conditional display (show/hide)
  • Capture user interactions and delegate to Controller
  • Never query the database or run business logic
Theme Template Files
// 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();
Shortcode Output
// 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');
Gutenberg Block save()
// 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>
    );
}