Request Lifecycle

Understanding when and how WPZylos boots helps you write efficient, well-timed code.

WordPress Request Timeline

timeline
    title WordPress Request Flow

    section Early
        mu-plugins loaded : Must-use plugins load
        plugins loaded : Regular plugins load
        after_setup_theme : Theme setup complete

    section Init
        init : WordPress fully initialized
        wp_loaded : All post-init hooks done

    section Request
        parse_request : URL parsed
        wp : Main query ready
        template_redirect : Template about to load

    section Render
        wp_head : Head content
        the_content : Post content
        wp_footer : Footer content
        shutdown : Request complete

WPZylos Boot Sequence

sequenceDiagram
    participant WP as WordPress
    participant Plugin as plugin.php
    participant App as Application
    participant Providers as ServiceProviders

    WP->>Plugin: Load plugin file
    Plugin->>Plugin: require vendor/autoload.php
    Plugin->>App: Create PluginContext
    Plugin->>App: Create Container
    Plugin->>App: Create Application

    Note over Plugin,App: Synchronous

    WP->>WP: plugins_loaded hook
    App->>Providers: Call register() on all
    App->>Providers: Call boot() on all

    WP->>WP: init hook
    Note over WP: Your hooks start firing

When to Use Each Hook

plugins_loaded

Services are registered but WordPress isn't fully loaded:

// In boot() - fires during plugins_loaded
public function boot(): void
{
    // Good: Safe: Register other hooks
    $this->hooks->wpAction('init', [$this, 'initialize']);

    // Bad: Unsafe: Current user may not be set
    $user = wp_get_current_user();
}

init

WordPress is fully loaded:

$hooks->wpAction('init', function () {
    // Good: Safe: Full WordPress API available
    $user = wp_get_current_user();

    // Good: Safe: Rewrite rules can be added
    add_rewrite_rule(...);

    // Good: Safe: Custom post types
    register_post_type(...);
});

admin_init

Admin is initialized:

$hooks->wpAction('admin_init', function () {
    // Good: Safe: Admin checks work
    if (!current_user_can('manage_options')) {
        return;
    }

    // Register admin-only functionality
});

wp_loaded

Everything is loaded, good for late initialization:

$hooks->wpAction('wp_loaded', function () {
    // All plugins loaded, theme loaded
    // Good for cross-plugin integration
});

Activation Lifecycle

Activation hooks run in a separate request:

sequenceDiagram
    participant Admin as Admin User
    participant WP as WordPress
    participant Plugin as plugin.php

    Admin->>WP: Click "Activate"
    WP->>Plugin: Load plugin file
    WP->>Plugin: Fire activation hook
    Plugin->>WP: Run activation logic
    WP->>Admin: Redirect to plugins page

    Note over WP,Plugin: Plugin NOT fully booted during activation

Lightweight Activation

register_activation_hook(__FILE__, function () {
    // Good: Do: Database setup
    global $wpdb;
    $wpdb->query("CREATE TABLE IF NOT EXISTS ...");

    // Good: Do: Set version
    update_option('mp_version', '1.0.0');

    // Good: Do: Schedule cron
    if (!wp_next_scheduled('mp_daily_task')) {
        wp_schedule_event(time(), 'daily', 'mp_daily_task');
    }

    // Bad: Don't: Boot full application
    // Bad: Don't: Resolve container services
    // Bad: Don't: Fire custom hooks
});

Why We Did It This Way

Problem: Heavy activation hooks cause timeouts, especially on shared hosting.

Solution: Activation only performs essential database operations. Full boot happens on the next regular request.

// Bad: Bad: Full boot in activation
register_activation_hook(__FILE__, function () {
    $app = new Application($context, $container);
    $app->boot();  // May timeout!
});

// Good: Good: Minimal activation
register_activation_hook(__FILE__, function () {
    update_option('mp_needs_setup', true);
});

// Full setup runs later
add_action('admin_init', function () {
    if (get_option('mp_needs_setup')) {
        // Run setup wizard
        delete_option('mp_needs_setup');
    }
});

Deactivation Lifecycle

register_deactivation_hook(__FILE__, function () {
    // Good: Do: Clear scheduled tasks
    wp_clear_scheduled_hook('mp_daily_task');

    // Good: Do: Clear transients
    delete_transient('mp_cache');

    // Bad: Don't: Delete user data
    // Bad: Don't: Drop database tables
});

Uninstall Lifecycle

Create uninstall.php in plugin root:

<?php
// uninstall.php

if (!defined('WP_UNINSTALL_PLUGIN')) {
    exit;
}

// Clean up database tables
global $wpdb;
$wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}mp_data");

// Clean up options
delete_option('mp_version');
delete_option('mp_settings');

// Clean up user meta
delete_metadata('user', 0, 'mp_preferences', '', true);

// Clean up transients
global $wpdb;
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mp_%'");

AJAX Request Lifecycle

sequenceDiagram
    participant Browser
    participant WP as WordPress
    participant Plugin as plugin.php
    participant Controller

    Browser->>WP: POST /wp-admin/admin-ajax.php
    WP->>Plugin: Load plugins
    WP->>WP: admin_init (skipped for AJAX)
    WP->>WP: Fire wp_ajax_{action}
    WP->>Controller: Handle request
    Controller->>Browser: JSON response

AJAX Best Practices

$hooks->wpAction('wp_ajax_mp_save', function () {
    // Verify nonce first
    if (!wp_verify_nonce($_POST['nonce'] ?? '', 'mp_save')) {
        wp_send_json_error('Invalid nonce', 403);
    }

    // Check capability
    if (!current_user_can('manage_options')) {
        wp_send_json_error('Unauthorized', 403);
    }

    // Handle request
    wp_send_json_success(['saved' => true]);
});

REST API Lifecycle

$hooks->wpAction('rest_api_init', function () {
    register_rest_route($this->context->slug() . '/v1', '/items', [
        'methods' => 'GET',
        'callback' => [$this, 'getItems'],
        'permission_callback' => fn() => current_user_can('read'),
    ]);
});

Performance Tips

Defer Heavy Operations

// Bad: Bad: Always runs
public function boot(): void
{
    $this->loadExpensiveData();
}

// Good: Good: Runs only when needed
public function boot(): void
{
    add_action('init', fn() => $this->loadExpensiveData());
}

Skip Admin in Frontend

public function boot(): void
{
    if (!is_admin()) {
        return;  // Skip admin-only code on frontend
    }

    // Admin initialization
}

Lazy Load Services

$container->singleton(ExpensiveService::class, function () {
    // Only instantiated when first requested
    return new ExpensiveService();
});

Next Steps