Hooks

When

Functions

How

Core

What

WordPress Core

The engine you never touch directly — but always extend. This is what runs before your first line of code executes.

WordPress Execution Lifecycle

What runs first?

Every WordPress request follows the same execution order. Understanding this is the foundation of professional WP development — and the most common interview question.

Boots.
#1 index.php Entry Point

The very first file executed. Loads wp-blog-header.php.

// index.php
require( dirname( __FILE__ ) . '/wp-blog-header.php' );
#2 wp-load.php Load WordPress

Defines ABSPATH, loads wp-config.php, then wp-settings.php.

define( 'ABSPATH', __DIR__ . '/' );
require_once ABSPATH . 'wp-settings.php';
#3 wp-settings.php muplugins_loaded
Core Constants & MU Plugins

Defines core constants, loads essential files, fires muplugins_loaded after must-use plugins.

do_action( 'muplugins_loaded' );
Plug.
#4 wp-settings.php plugins_loaded
Plugins Loaded

All active plugins are included. plugins_loaded fires — safest hook for plugin-to-plugin interaction.

// Plugins included here
do_action( 'plugins_loaded' );
#5 wp-settings.php setup_theme Setup Theme

Before the theme is loaded. Last chance to override the theme.

do_action( 'setup_theme' );
#6 wp-settings.php after_setup_theme
After Setup Theme

Theme functions.php is loaded and after_setup_theme fires. Theme features (add_theme_support) go here.

// functions.php loaded
do_action( 'after_setup_theme' );
Init.
#7 wp-settings.php init
Init

WordPress is fully loaded. Register post types, taxonomies, shortcodes here.

do_action( 'init' );
#8 wp-settings.php wp_loaded
WP Loaded

Everything is loaded including init. Used by WooCommerce, ACF and similar.

do_action( 'wp_loaded' );
Query.
#9 wp-blog-header.php parse_request Parse Request

WordPress parses the URL into query variables.

$wp->parse_request();
#10 class-wp.php send_headers Send Headers

HTTP headers are sent. Last chance to set headers.

do_action( 'send_headers' );
#11 class-wp.php parse_query pre_get_posts
Query Parsed

Main WP_Query is built. pre_get_posts allows modifying the query before DB hit.

do_action( 'parse_query', $this );
do_action( 'pre_get_posts', $query );
#12 class-wp.php wp WP Hook

Main query is set. $wp_query is populated. Template selection begins.

do_action( 'wp', $wp );
Tmpl.
#13 template-loader.php template_redirect
Template Redirect

Before template is loaded. Redirect users, force different templates.

do_action( 'template_redirect' );
#14 template-loader.php template_include
Template Include

Filter which template file gets loaded.

$template = apply_filters( 'template_include', $template );
#15 header.php wp_head
WP Head

Inside <head>. Enqueue styles, add meta tags, Google Analytics.

do_action( 'wp_head' );
#16 footer.php wp_footer
WP Footer

Before </body>. Footer scripts, analytics, deferred JS.

do_action( 'wp_footer' );
#17 wp-includes/class-wp-hook.php shutdown Shutdown

Very last hook. Clean up, log, close connections.

do_action( 'shutdown' );

Core API Groups

The Toolbox

WordPress exposes its functionality through grouped APIs. Each is designed to abstract complexity and ensure compatibility.

Reading and writing data to the database.

  • Posts API

    get_post(), get_posts(), WP_Query

    Retrieve single or multiple posts

    Docs
  • Meta API

    get_post_meta(), update_post_meta()

    Post, user, term and comment metadata

    Docs
  • Options API

    get_option(), update_option()

    Site-wide settings and persistent data

    Docs
  • Transients API

    get_transient(), set_transient()

    Cached temporary data with expiry

    Docs
  • User API

    get_user_by(), wp_insert_user()

    User management and authentication

    Docs

Hooks Map

Where to hook in?

All essential WordPress hooks grouped by execution phase. Actions execute code; filters modify data.

Bootstrap Phase
Init Phase
Query Phase
  • parse_request URL parsed
  • pre_get_posts Before DB query
  • parse_query Query vars set
  • wp Main query done
Template Phase
Data Phase

Best Practices

The Rules

Principles that separate WordPress developers from WordPress professionals.

Never modify Core files Critical

Any change to wp-includes/ or wp-admin/ will be overwritten on the next WordPress update. Use hooks instead.

Bad

// NEVER do this
// wp-includes/functions.php
function wp_insert_post( $postarr, ... ) {
  // your custom code here — GONE after update
}

Good

// Hook into it instead
add_action( 'save_post', function( $post_id ) {
  // runs after wp_insert_post — survives updates
  do_something( $post_id );
});
Prefix everything Critical

Function names, class names, hooks, option keys — all must have a unique prefix to avoid collisions with Core and other plugins.

Bad

// Too generic — will collide
function get_data() { ... }
add_action( 'init', 'setup' );
$options = get_option( 'settings' );

Good

// Namespaced / prefixed
function myplugin_get_data() { ... }
add_action( 'init', 'myplugin_setup' );
$options = get_option( 'myplugin_settings' );
Sanitize input, Escape output Critical

Always sanitize data when it enters WordPress (from $_POST, $_GET, DB). Always escape data when it leaves (HTML, attributes, SQL).

Bad

// XSS and SQL injection risk
$name = $_POST['name'];
echo $name;
$query = "SELECT * FROM wp_posts WHERE ID = $id";

Good

// Safe input/output
$name = sanitize_text_field( $_POST['name'] );
echo esc_html( $name );
$query = $wpdb->prepare(
  "SELECT * FROM wp_posts WHERE ID = %d", $id
);
Never use query_posts() Warning

query_posts() replaces the main query, causing pagination breaks and performance issues. Use WP_Query or pre_get_posts.

Bad

// Breaks pagination and main loop
query_posts( 'post_type=product&posts_per_page=10' );

Good

// Use WP_Query for secondary queries
$products = new WP_Query( [
  'post_type'      => 'product',
  'posts_per_page' => 10,
] );

// Use pre_get_posts to modify the MAIN query
add_action( 'pre_get_posts', function( $query ) {
  if ( ! is_admin() && $query->is_main_query() ) {
    $query->set( 'posts_per_page', 10 );
  }
});
Always verify nonces Critical

Nonces protect forms and AJAX requests from CSRF attacks. Always generate and verify them.

Bad

// No nonce check — CSRF vulnerable
if ( isset( $_POST['save'] ) ) {
  update_post_meta( $id, 'key', $_POST['val'] );
}

Good

// Generate nonce in form
wp_nonce_field( 'myplugin_save_meta', 'myplugin_nonce' );

// Verify on save
if ( ! wp_verify_nonce( $_POST['myplugin_nonce'], 'myplugin_save_meta' ) ) {
  wp_die( 'Security check failed' );
}
Check capabilities before actions Critical

Always verify that the current user has the right permissions before performing sensitive operations.

Bad

// Any logged-in user can delete!
add_action( 'admin_post_delete_item', function() {
  delete_post( $_POST['id'] );
});

Good

// Check capability first
add_action( 'admin_post_delete_item', function() {
  if ( ! current_user_can( 'delete_posts' ) ) {
    wp_die( 'Insufficient permissions' );
  }
  delete_post( absint( $_POST['id'] ) );
});
Cache expensive queries with Transients Performance

Expensive WP_Query calls, remote API requests, or complex calculations should be cached using the Transients API.

Bad

// Hits DB on every page load
$results = new WP_Query( [
  'post_type'      => 'product',
  'posts_per_page' => -1,
  'meta_query'     => [...],
] );

Good

// Cache for 1 hour
$results = get_transient( 'myplugin_products' );
if ( false === $results ) {
  $query   = new WP_Query( [...] );
  $results = $query->posts;
  set_transient( 'myplugin_products', $results, HOUR_IN_SECONDS );
}
Use $wpdb for custom queries Warning

When you need custom SQL, use $wpdb with prepared statements — never raw SQL with user input.

Bad

// SQL injection risk
$results = mysql_query(
  "SELECT * FROM custom_table WHERE user = '$user'"
);

Good

// Safe with $wpdb->prepare()
global $wpdb;
$results = $wpdb->get_results(
  $wpdb->prepare(
    "SELECT * FROM {$wpdb->prefix}custom_table WHERE user = %s",
    $user
  )
);

Explore more

Now dive into Hooks & Functions

You know what the Core does. Now learn how to extend it.