Ready to get started?

Check out the plugin on GitHub and start using it today.

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>
    );
}