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
- Service Providers — Where lifecycle code lives
- Hook System — Hooking into lifecycle events