Request Lifecycle and Load Order
High-Level Lifecycle
For a normal front-end request, WordPress roughly does this:
| Step | What Happens |
|---|---|
| 1 | Loads configuration and core files |
| 2 | Loads must-use plugins |
| 3 | Loads active plugins |
| 4 | Loads the active theme functions file |
| 5 | Parses the request URL |
| 6 | Builds the main query |
| 7 | Chooses a template file |
| 8 | Sends the response |
This order explains why plugins are good for site behavior and themes are good for presentation.
Important Hooks During Load
| Hook | Typical Use |
|---|---|
plugins_loaded | Load plugin dependencies or translations |
init | Register post types, taxonomies, shortcodes |
wp_enqueue_scripts | Load front-end CSS and JavaScript |
pre_get_posts | Adjust the main query before it runs |
template_redirect | Redirect or handle front-end request logic |
admin_menu | Register admin menu pages |
admin_init | Register settings and admin handlers |
Plugin Code Loads Early
Plugin files load before the active theme. That makes plugins the right place for content types, integrations, custom REST routes, and business rules.
add_action('init', 'myplugin_register_book_type');
function myplugin_register_book_type(): void {
register_post_type('book', [
'label' => __('Books', 'my-plugin'),
'public' => true,
'has_archive' => true,
]);
}
Theme Code Loads for Presentation
The active theme functions.php can register theme supports, menus, image sizes, and enqueue assets.
add_action('after_setup_theme', 'mytheme_setup');
function mytheme_setup(): void {
add_theme_support('post-thumbnails');
register_nav_menus([
'primary' => __('Primary Menu', 'mytheme'),
]);
}
Main Query
WordPress converts the URL into query variables, then queries posts. Template files display the result.
Use pre_get_posts to adjust the main query, not a second query inside the template.
add_action('pre_get_posts', 'mytheme_limit_archive_posts');
function mytheme_limit_archive_posts(WP_Query $query): void {
if (is_admin() || ! $query->is_main_query() || ! $query->is_archive()) {
return;
}
$query->set('posts_per_page', 12);
}
Common Pitfalls
- Running front-end-only logic in admin requests
- Registering post types too late
- Querying posts in templates when
pre_get_postsshould adjust the main query - Assuming theme code is available to plugin code during plugin load
What's Next
Deep WordPress Application
This lesson is most useful when applied to real WordPress code rather than isolated PHP examples. Request Lifecycle and Load Order affects how a site behaves under plugins, themes, editors, logged-in users, guests, cached pages, REST requests, and production traffic.
The WordPress execution model determines whether code runs early enough, late enough, once per request, or many times in a loop.
A practical implementation should answer four questions before code is written:
- Which WordPress request context will run this code?
- Which API or hook owns the behavior?
- Which data is trusted, and which data must be normalized?
- What should happen when the expected data, permission, or dependency is missing?
WordPress API Anchor
For this topic, a common API or integration point is add_action(). The exact function may vary by feature, but the design principle is stable: use WordPress APIs before inventing custom plumbing.
A common hook or lifecycle point for this topic is template_redirect. Confirm the hook runs in the request type you care about before attaching expensive or state-changing logic.
Focused Code Pattern
add_action('wp', 'myplugin_inspect_request');
function myplugin_inspect_request(): void {
if (is_admin() || ! is_singular()) {
return;
}
error_log('Viewing singular content: ' . get_queried_object_id());
}
This pattern should still be adapted to the exact feature. Add capability checks for private data, nonces for browser-submitted state changes, and output escaping when rendering values.
Data Flow Walkthrough
| Stage | What To Decide | WordPress Example |
|---|---|---|
| Source | Where the value comes from | Request data, option, post meta, user meta, REST parameter, remote API |
| Trust | Whether the value can be used directly | Treat external, request, and old saved values as untrusted |
| Normalize | How PHP converts the value into a safe shape | absint(), sanitize_text_field(), sanitize_key(), custom allow-list |
| Authorize | Who can read or change it | current_user_can() with a specific capability |
| Persist | Where the value belongs | Option, post meta, user meta, taxonomy term, custom table |
| Render | How the value leaves PHP | esc_html(), esc_attr(), esc_url(), wp_kses_post() |
Applied Practice
Attach one callback to an early hook and one callback to a later hook, then compare which conditional tags are reliable.
When practicing, do not stop when the happy path works. WordPress code becomes reliable when the failure paths are equally deliberate.
Expanded Decision Guide
| Decision | Prefer This | Avoid This |
|---|---|---|
| Where code lives | A plugin for durable behavior, a theme for presentation | Editing WordPress core or vendor plugin files |
| How data enters | A named request handler with validation | Reading superglobals deep inside templates |
| How data is stored | WordPress APIs with clear keys and types | Unstructured arrays with undocumented meanings |
| How output is printed | Escape at the final output context | Trusting saved data because it came from the database |
| How failures behave | Return early, log safely, show useful messages | Fatal errors, blank pages, or exposed stack details |
| How changes ship | Small reviewed changes with rollback notes | Large untested edits directly on production |
Implementation Checklist
Use this checklist when applying the lesson in a real WordPress codebase.
- Identify whether the code belongs in a plugin, child theme, block, mu-plugin, or deployment script.
- Name functions, classes, options, actions, filters, and CSS hooks with a project-specific prefix.
- Confirm which hook should run the code and whether that hook fires in admin, front end, AJAX, REST, cron, or CLI contexts.
- Validate every value that comes from a request, database option, custom field, remote API, cookie, or file.
- Sanitize before storing data and escape at the exact output boundary.
- Check the narrowest useful capability before reading private data or changing state.
- Add nonces or signed requests for state-changing browser actions.
- Keep database queries narrow by requesting only the fields and post types needed.
- Reset global WordPress state after custom queries, site switching, or temporary filters.
- Add a short manual test note for the main success path and at least one failure path.
Extended Example Pattern
The following pattern is intentionally small. It demonstrates how to keep request handling, normalization, and output separate enough to review.
add_action('admin_post_myplugin_example_action', 'myplugin_handle_example_action');
function myplugin_handle_example_action(): void {
if (! current_user_can('manage_options')) {
wp_die(esc_html__('You are not allowed to do that.', 'my-plugin'));
}
check_admin_referer('myplugin_example_action', 'myplugin_nonce');
$label = isset($_POST['label'])
? sanitize_text_field(wp_unslash($_POST['label']))
: '';
if ('' === $label) {
wp_safe_redirect(add_query_arg('status', 'missing-label', wp_get_referer() ?: admin_url()));
exit;
}
update_option('myplugin_example_label', $label, false);
wp_safe_redirect(add_query_arg('status', 'saved', wp_get_referer() ?: admin_url()));
exit;
}
Troubleshooting Matrix
| Symptom | Likely Cause | First Check |
|---|---|---|
| Callback never runs | Wrong hook name, priority, or load context | Confirm the hook fires in this request type |
| Data saves incorrectly | Missing unslash, sanitization, or type normalization | Log sanitized values in development only |
| Output looks broken | Escaped for the wrong context or escaped too early | Check whether the output is text, HTML, URL, or attribute |
| Permission bug | Role check is too broad or capability is missing an object ID | Use current_user_can() with the specific operation |
| Slow page | Query, HTTP request, or loop work runs on every request | Profile with Query Monitor and add caching or batching |
| Works locally only | PHP version, plugin dependency, rewrite rule, or environment setting differs | Compare environment versions and enabled plugins |
Practice Exercise
- Recreate the smallest practical example from this lesson in a local WordPress site.
- Add one valid input and one invalid input.
- Confirm the invalid input fails safely without changing stored data.
- Confirm the valid input is sanitized before storage.
- Render the stored value in at least two contexts, such as text and attribute output.
- Add a capability check and verify a lower-privilege user cannot perform the action.
- Temporarily enable debug logging and confirm no PHP warnings appear.
- Remove temporary logs before treating the example as complete.
Review Questions
- What WordPress hook or API makes this lesson reliable in the right request context?
- Which values in the example are untrusted at the moment they enter PHP?
- Where should validation happen, and where should output escaping happen?
- What user capability is required for the action, and why is that the narrowest useful choice?
- What is the expected behavior when the required data is missing or malformed?
- What would need to change before this code runs safely on a large production site?
Production Notes
Production WordPress PHP should be easy to audit under pressure. Keep the control flow direct, keep side effects visible, and prefer small functions that name the business rule they enforce. If a future maintainer cannot identify the request source, permission check, data normalization, and output boundary in a few minutes, the code is too implicit.