🏛️ 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_poststable 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.
| Component | Responsibility | Building analogy |
|---|---|---|
| Database | Stores posts, users, options, and metadata in MySQL/MariaDB | The records office |
| Core files | The PHP engine: bootstrapping, queries, template loading | Structural systems |
| Themes | Control presentation via the template hierarchy | Interior design & floor plan |
| Plugins | Add functionality through hooks | Modular add-ons |
| Admin dashboard | The UI for managing everything | The building control room |
| REST API | HTTP endpoints for external apps & the block editor | Service 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.
| Table | Stores | Key columns |
|---|---|---|
wp_posts | All content types | ID, post_title, post_content, post_type, post_status |
wp_postmeta | Custom fields for posts | post_id, meta_key, meta_value |
wp_users | User accounts | ID, user_login, user_pass, user_email |
wp_usermeta | User capabilities & prefs | user_id, meta_key, meta_value |
wp_comments / wp_commentmeta | Comments and their metadata | comment_post_ID, comment_content |
wp_terms | Category/tag/term names | term_id, name, slug |
wp_term_taxonomy | Which taxonomy a term belongs to | term_id, taxonomy |
wp_term_relationships | Links posts to terms | object_id, term_taxonomy_id |
wp_options | Site-wide settings | option_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.
- Entry & rewrite — the server routes the request to
index.phpusing.htaccess(Apache) or a location block (Nginx). - Bootstrap —
wp-load.php→wp-config.php→wp-settings.phpload config, connect to the database, and set up core functions. - Plugins & theme load — active plugins are included and the
plugins_loaded, theninithooks fire. - Query parsing —
WP_Queryturns the URL into query variables (here, "single post with slugsample-post"). - Content query — SQL runs against the database to fetch the matching rows.
- Template selection — the template hierarchy picks a file (e.g.
single.php). - Rendering — The Loop iterates results and template tags emit HTML.
- 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.
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.
| API | Purpose | Signature functions |
|---|---|---|
| Plugin (Hooks) | Extend/modify behaviour | add_action, add_filter |
| Options | Site-wide settings | get_option, update_option |
| Metadata | Custom fields on posts/users/etc. | get_post_meta, update_post_meta |
| HTTP | Call external services | wp_remote_get, wp_remote_post |
| Database | Direct SQL (last resort) | $wpdb->get_results, $wpdb->prepare |
| Shortcode | Content macros | add_shortcode |
| REST | HTTP endpoints | register_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
- Spin up a local WordPress (LocalWP, Docker, or XAMPP — covered in a later lesson).
- In
wp-content/plugins/, createhook-detective.phpwith the code below. - Activate it in Plugins, make sure
WP_DEBUG_LOGis on, then load your homepage and a single post. - Open
wp-content/debug.logand 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/orwp-includes/— updates erase them. - Trust
$_GET/$_POSTdata or drop it into SQL unprepared. - Forget to
returnfrom a filter callback (it silently blanks the value). - Reach for
$wpdbwhen 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_poststable stores all content types viapost_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
- WordPress Developer Resources
- Plugin Handbook — Hooks
- Common APIs — Security
- WP_Query Class Reference
🚀 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.