Skip to main content

🏛️ WordPress Core Architecture

WordPress powers roughly 43% of all websites, yet most people who use it never see how it works underneath. In this lesson you'll open the hood: the database that holds every post, the files that run the show, the exact sequence a request follows, and the hooks system that makes WordPress endlessly extensible without ever touching core.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Describe the core components of WordPress and how the database, file system, and PHP core cooperate
  • Read the WordPress database schema and explain how one wp_posts table stores many content types
  • Trace the request lifecycle from a browser URL to rendered HTML
  • Use the hooks system (actions and filters) to extend WordPress safely
  • Identify the core APIs and built-in security features you should rely on

Estimated Time: 45–55 minutes  •  Difficulty: Intermediate

Hands-on: Build a tiny "hook detective" plugin and watch the WordPress lifecycle fire in real time.

In This Lesson

What WordPress Really Is

WordPress began in 2003 as a blogging tool. Twenty years later it is a full-featured content management system (CMS) released under the GPL, running everything from personal journals to enterprise publishing platforms and e-commerce stores. Its dominance is not an accident — it rests on a small set of architectural decisions that traded raw purity for practical flexibility.

💡 A useful analogy: Think of WordPress like a modern apartment building. There is shared core infrastructure you never modify (plumbing, wiring, the elevator = core files), a records office that tracks who lives where (the database), and units that tenants decorate freely (themes and plugins). You customize your unit; you don't rewire the building.

Four properties explain why it caught on and stayed on top:

  • Open-source — free to use, modify, and distribute; no vendor lock-in.
  • Extensible — a hooks system lets plugins and themes change behaviour without editing core.
  • Accessible — non-technical editors publish content through a friendly admin dashboard.
  • Mature — two decades of hardening, an enormous community, and predictable release cycles.

Understanding the architecture is the difference between using WordPress and building on it. Everything in this module builds on the mental model you form here.

Core Components

WordPress is best understood as a handful of cooperating subsystems. A single incoming request touches nearly all of them.

flowchart TB A["WordPress Core (PHP)"] --> B[Database] A --> C[File System] A --> D[Request Handling] B --> B1["MySQL / MariaDB"] C --> C1[Themes] C --> C2[Plugins] C --> C3[Core Files] C --> C4["Media / Uploads"] D --> D1["Routing and Rewrite"] D --> D2[Template Loading] D --> D3[The Loop] D --> D4[Hooks System]
ComponentResponsibilityBuilding analogy
DatabaseStores posts, users, options, and metadata in MySQL/MariaDBThe records office
Core filesThe PHP engine: bootstrapping, queries, template loadingStructural systems
ThemesControl presentation via the template hierarchyInterior design & floor plan
PluginsAdd functionality through hooksModular add-ons
Admin dashboardThe UI for managing everythingThe building control room
REST APIHTTP endpoints for external apps & the block editorService entrances

📖 Key Terms

Core: the files WordPress ships with — wp-admin/ and wp-includes/. Never edit them; updates overwrite them.

Bootstrap: the startup sequence that loads config, connects to the database, and readies WordPress to handle a request.

Hook: a named point in execution where your code can run (an action) or modify data (a filter).

The Database Schema

A fresh WordPress install creates twelve tables. The clever trick is that a single table — wp_posts — stores posts, pages, attachments, revisions, menu items, and every custom post type, distinguished only by the post_type column. This "unified content model" keeps the schema simple while supporting rich content.

erDiagram wp_posts ||--o{ wp_postmeta : has wp_posts ||--o{ wp_comments : receives wp_posts ||--o{ wp_term_relationships : "tagged by" wp_comments ||--o{ wp_commentmeta : has wp_users ||--o{ wp_posts : authors wp_users ||--o{ wp_usermeta : has wp_terms ||--|| wp_term_taxonomy : "typed as" wp_term_taxonomy ||--o{ wp_term_relationships : connects
TableStoresKey columns
wp_postsAll content typesID, post_title, post_content, post_type, post_status
wp_postmetaCustom fields for postspost_id, meta_key, meta_value
wp_usersUser accountsID, user_login, user_pass, user_email
wp_usermetaUser capabilities & prefsuser_id, meta_key, meta_value
wp_comments / wp_commentmetaComments and their metadatacomment_post_ID, comment_content
wp_termsCategory/tag/term namesterm_id, name, slug
wp_term_taxonomyWhich taxonomy a term belongs toterm_id, taxonomy
wp_term_relationshipsLinks posts to termsobject_id, term_taxonomy_id
wp_optionsSite-wide settingsoption_name, option_value, autoload

The wp_ prefix is configurable at install time (a small security nicety). Because everything shares a few flexible tables, WordPress can model complex relationships — a post authored by a user, filed under two categories, carrying five custom fields — without a sprawling schema.

⚠️ Don't query the database directly by hand

It is tempting to write raw SELECTs against wp_posts. Prefer WP_Query and the metadata/options APIs — they respect caching, security, and future schema changes. Reach for $wpdb only when no higher-level API exists, and always use $wpdb->prepare().

The File Structure

The file system separates core (replaced on every update) from your content (preserved). This single boundary is why WordPress can auto-update without wiping your customizations.

wordpress/
├── wp-admin/          # Admin dashboard (core — never edit)
├── wp-includes/       # Core functionality (core — never edit)
├── wp-content/        # YOUR stuff (preserved across updates)
│   ├── themes/        # Theme directories
│   ├── plugins/       # Plugin directories
│   ├── mu-plugins/    # "Must-use" plugins, auto-activated
│   ├── uploads/       # Media, organized by year/month
│   └── languages/     # Translation files
├── index.php          # Front-controller entry point
├── wp-config.php      # DB credentials, salts, constants
├── wp-load.php        # Locates and loads wp-config.php
├── wp-settings.php    # Bootstraps the whole environment
└── .htaccess          # Apache rewrite rules (pretty permalinks)

✅ The golden rule

Do all your work inside wp-content/. Themes change presentation; plugins change behaviour. Both survive updates. Editing anything in wp-admin/ or wp-includes/ means your changes vanish the next time WordPress updates — and it will.

Of the root files, only wp-config.php is meant to be edited directly. It holds your database credentials, secret keys, and configuration constants. Everything else in the root is core plumbing that runs the bootstrap.

The Request Lifecycle

When someone visits a URL, WordPress runs a well-defined sequence to turn that request into HTML. Knowing this order tells you where and when your own code can intervene.

sequenceDiagram participant U as Browser participant S as Web Server participant I as index.php participant W as WP Core participant DB as Database participant T as Theme U->>S: HTTP request for /sample-post/ S->>I: Rewrite to index.php I->>W: Load wp-config & wp-settings W->>DB: Connect W->>W: Load plugins (init, plugins_loaded) W->>W: Parse URL into a query (WP_Query) W->>DB: Fetch matching posts W->>T: Choose template (hierarchy) T->>U: Render HTML via The Loop
  1. Entry & rewrite — the server routes the request to index.php using .htaccess (Apache) or a location block (Nginx).
  2. Bootstrapwp-load.phpwp-config.phpwp-settings.php load config, connect to the database, and set up core functions.
  3. Plugins & theme load — active plugins are included and the plugins_loaded, then init hooks fire.
  4. Query parsingWP_Query turns the URL into query variables (here, "single post with slug sample-post").
  5. Content query — SQL runs against the database to fetch the matching rows.
  6. Template selection — the template hierarchy picks a file (e.g. single.php).
  7. RenderingThe Loop iterates results and template tags emit HTML.
  8. Output — filters get one last chance to modify markup before it is sent.

At almost every step, WordPress fires hooks. That is the seam plugins slip into — which is exactly what we cover next.

The Hooks System

Hooks are the beating heart of WordPress extensibility. As core runs, it repeatedly pauses and announces, "if anyone wants to do something here, now's the moment." Your plugin registers a callback and joins in — no core edits required.

How a WordPress hook dispatches to registered callbacks WordPress core reaches a hook, which fans out to several registered plugin and theme callbacks in priority order, then execution returns to core. WP Core reaches do_action Hook 'save_post' Plugin callback (prio 10) Plugin callback (prio 20) Theme callback (prio 30)
Figure 1 — One hook can dispatch to many callbacks. WordPress runs them in ascending priority order, then continues.

Actions — do something

Action hooks let you run code at a point in execution. They don't return a value; they just act. Think of them as event notifications: "a post was just saved — react if you like."

<?php
// Run after a post is saved. Signature: ($post_id, $post, $update)
add_action( 'save_post', 'mycb_touch_edit_time', 10, 3 );

function mycb_touch_edit_time( int $post_id, WP_Post $post, bool $update ): void {
    if ( $update && $post->post_type === 'post' ) {
        update_post_meta( $post_id, 'last_edited_time', time() );
    }
}

Filters — change something

Filter hooks pass data through your callback so you can modify it. The one rule: always return a value. Think of a water filter — data flows in, altered data flows out.

<?php
// Append a call-to-action to the end of single posts.
add_filter( 'the_content', 'mycb_append_cta' );

function mycb_append_cta( string $content ): string {
    if ( is_single() && ! is_admin() ) {
        $content .= '<div class="cta">Thanks for reading! Please subscribe.</div>';
    }
    return $content; // Filters MUST return.
}

📖 The four functions to remember

Register: add_action() / add_filter() — attach your callback.

Fire: do_action() / apply_filters() — core (or you) trigger the hook.

The last two arguments of add_* are priority (default 10; lower runs first) and accepted args (how many parameters your callback wants).

Commonly used hooks you will meet constantly: init, wp_enqueue_scripts, wp_head, wp_footer, the_content, the_title, pre_get_posts, admin_menu, and save_post.

Core APIs

WordPress ships standardized APIs for common jobs. Using them (instead of hand-rolling) keeps your code update-safe and compatible with other plugins.

APIPurposeSignature functions
Plugin (Hooks)Extend/modify behaviouradd_action, add_filter
OptionsSite-wide settingsget_option, update_option
MetadataCustom fields on posts/users/etc.get_post_meta, update_post_meta
HTTPCall external serviceswp_remote_get, wp_remote_post
DatabaseDirect SQL (last resort)$wpdb->get_results, $wpdb->prepare
ShortcodeContent macrosadd_shortcode
RESTHTTP endpointsregister_rest_route

Options in practice

<?php
// Read with a fallback default.
$per_page = get_option( 'mycb_items_per_page', 12 );

// Write (creates the row if it doesn't exist).
update_option( 'mycb_items_per_page', 24 );

A safe external HTTP call

<?php
$response = wp_remote_get( 'https://api.example.com/data' );

if ( is_wp_error( $response ) ) {
    error_log( 'API call failed: ' . $response->get_error_message() );
} else {
    $data = json_decode( wp_remote_retrieve_body( $response ), true );
    // ...use $data
}

A shortcode

<?php
add_shortcode( 'greeting', 'mycb_greeting' );

function mycb_greeting( array $atts ): string {
    $atts = shortcode_atts( [ 'name' => 'World' ], $atts, 'greeting' );
    return '<p>Hello, ' . esc_html( $atts['name'] ) . '!</p>';
}
// Usage in content: [greeting name="Ray"]

Security Architecture

Security is baked into WordPress core, but it is a shared responsibility: the tools exist, and your code must use them. Four pillars matter most.

1. Prepared statements (stop SQL injection)

<?php
global $wpdb;

// SAFE — user input is bound, never concatenated.
$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s",
        'post',
        'publish'
    )
);

⚠️ Never do this

<?php
// VULNERABLE — raw user input dropped straight into SQL.
$status  = $_GET['status'];
$results = $wpdb->get_results( "SELECT * FROM {$wpdb->posts} WHERE post_status = '$status'" );

2. Nonces (stop CSRF)

<?php
// In the form:
wp_nonce_field( 'delete_post_' . $post_id, 'security' );

// When processing the submission:
if ( ! isset( $_POST['security'] )
     || ! wp_verify_nonce( $_POST['security'], 'delete_post_' . (int) $_POST['post_id'] ) ) {
    wp_die( 'Security check failed' );
}

3. Capabilities (authorize the user)

<?php
if ( current_user_can( 'edit_post', $post_id ) ) {
    // Safe to show the edit link / perform the edit.
}

4. Sanitize input, escape output

<?php
// Sanitize on the way IN:
$name  = sanitize_text_field( $_POST['name'] );
$email = sanitize_email( $_POST['email'] );

// Escape on the way OUT:
echo '<a href="' . esc_url( $url ) . '">' . esc_html( $name ) . '</a>';

The mantra: "Sanitize early, escape late, always validate." Common helpers include sanitize_text_field(), absint(), wp_kses_post(), esc_html(), esc_url(), and esc_attr().

Hands-on: Hook Detective

🏋️ Watch the lifecycle fire

Objective: Build a one-file plugin that logs core hooks as they fire, then correlate the log with the lifecycle you learned above.

Instructions

  1. Spin up a local WordPress (LocalWP, Docker, or XAMPP — covered in a later lesson).
  2. In wp-content/plugins/, create hook-detective.php with the code below.
  3. Activate it in Plugins, make sure WP_DEBUG_LOG is on, then load your homepage and a single post.
  4. Open wp-content/debug.log and note the order the hooks fired in.
💡 Hint

plugins_loaded fires before init, which fires before wp_head, which fires before the_content, which fires before wp_footer. If your log is empty, confirm WP_DEBUG and WP_DEBUG_LOG are both true in wp-config.php.

✅ Solution
<?php
/**
 * Plugin Name: Hook Detective
 * Description: Logs when common WordPress hooks fire, in order.
 * Version: 1.0.0
 */

// A tiny factory so each hook logs its own name.
function hd_logger( string $hook ): callable {
    return static function () use ( $hook ) {
        error_log( "Hook fired: {$hook}" );
    };
}

foreach ( [ 'plugins_loaded', 'init', 'wp_loaded', 'wp_head', 'wp_footer' ] as $hook ) {
    add_action( $hook, hd_logger( $hook ) );
}

// Filters must return their value:
add_filter( 'the_content', static function ( string $content ): string {
    error_log( 'Hook fired: the_content' );
    return $content;
} );

// Bonus: see which template file rendered the page.
add_filter( 'template_include', static function ( string $template ): string {
    error_log( 'Template loaded: ' . $template );
    return $template;
} );

Your log will read roughly: plugins_loaded → init → wp_loaded → template_include → wp_head → the_content → wp_footer. That IS the request lifecycle, observed live.

🎯 Quick Quiz

Question 1: Which table stores blog posts, pages, and custom post types alike?

Question 2: What is the essential difference between an action and a filter?

Question 3: You need to run a raw SQL query with user-supplied input. What must you use?

Best Practices

✅ Do

  • Keep all custom code in wp-content/ (themes and plugins).
  • Use core APIs (WP_Query, options, metadata) instead of raw SQL.
  • Prefix your functions, hooks, and meta keys to avoid collisions.
  • Sanitize every input; escape every output; verify nonces and capabilities.

⚠️ Don't

  • Edit files in wp-admin/ or wp-includes/ — updates erase them.
  • Trust $_GET/$_POST data or drop it into SQL unprepared.
  • Forget to return from a filter callback (it silently blanks the value).
  • Reach for $wpdb when a higher-level API already does the job.

Summary & Quiz

🎉 Key Takeaways

  • WordPress is a mature CMS built on PHP + MySQL, extensible through hooks.
  • A single wp_posts table stores all content types via post_type — the unified content model.
  • Files split into untouchable core and preserved wp-content; do your work in the latter.
  • Every request follows a fixed lifecycle, firing hooks you can attach to.
  • Actions run code; filters transform and return data.
  • Security — prepared statements, nonces, capabilities, sanitize/escape — is built in but yours to apply.

📚 Further Reading

🚀 What's Next?

Now that you know how WordPress is built, the next lesson zooms in on how it manages content — post types, taxonomies, custom fields, and the query system you'll use every day.

🎉 Well done!

You can now read the WordPress database, trace a request, and extend the platform without touching core.