Dashboard widgets
wp_add_dashboard_widget( $widget_id, $widget_name, $callback, $control_callback, $callback_args );
$widget_id: an identifying slug for your widget. This will be used as its CSS class and its key in the array of widgets.$widget_name: this is the name your widget will display in its heading.$callback: The name of a function you will create that will display the actual contents of your widget.$control_callback(Optional): The name of a function you create that will handle submission of widget options forms, and will also display the form elements.$callback_args(Optional): Set of arguments for the callback function.
function wporg_add_dashboard_widgets() {
// Add function here
}
add_action( 'wp_dashboard_setup', 'wporg_add_dashboard_widgets' );
# wp_network_dashboard_setup to use in network dashboard
function wporg_add_dashboard_widgets() {
wp_add_dashboard_widget(
'wporg_dashboard_widget', // Widget slug.
esc_html__( 'Example Dashboard Widget', 'wporg' ), // Title.
'wporg_dashboard_widget_render' // Display function.
);
}
add_action( 'wp_dashboard_setup', 'wporg_add_dashboard_widgets' );
function wporg_dashboard_widget_render() {
esc_html_e( "Howdy! I'm a great Dashboard Widget.", "wporg" );
}
- Removing default widget
// Create the function to use in the action hook
function wporg_remove_dashboard_widget() {
remove_meta_box( 'dashboard_quick_press', 'dashboard', 'side' );
}
// Hook into the 'wp_dashboard_setup' action to register our function
add_action( 'wp_dashboard_setup', 'wporg_remove_dashboard_widget' );
# use these instad of wp_add_dashboard_widget fn to place at custom position
add_meta_box(
'dashboard_widget_id',
esc_html__( 'Dashboard Widget Title', 'wporg' ),
'dashboard_widget',
'dashboard',
'side', 'high'
);
delete_expired_transientsaction is used to call function of same name, used to schedule deletion of expired transients daily.
A widget adds content and features to a widget area (also called a sidebar). A widget is a PHP object that outputs some HTML. The same kind of widget can be used multiple times on the same page (e.g. the Text Widget). Widgets can save data in the database (in the options table).
a widget comprises two areas:
- Title Area
- Widget Options
<?php
class My_Widget extends WP_Widget {
public function __construct() {
parent::__construct(
'my-text', // Base ID
'My Text' // Name
);
add_action( 'widgets_init', function() {
register_widget( 'My_Widget' );
});
}
public $args = array(
'before_title' => '<h4 class="widgettitle">',
'after_title' => '</h4>',
'before_widget' => '<div class="widget-wrap">',
'after_widget' => '</div></div>',
);
public function widget( $args, $instance ) {
echo $args['before_widget'];
if ( ! empty( $instance['title'] ) ) {
echo $args['before_title'] . apply_filters( 'widget_title', $instance['title'] ) . $args['after_title'];
}
echo '<div class="textwidget">';
echo esc_html__( $instance['text'], 'text_domain' );
echo '</div>';
echo $args['after_widget'];
}
public function form( $instance ) {
$title = ! empty( $instance['title'] ) ? $instance['title'] : esc_html__( '', 'text_domain' );
$text = ! empty( $instance['text'] ) ? $instance['text'] : esc_html__( '', 'text_domain' );
?>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php echo esc_html__( 'Title:', 'text_domain' ); ?></label>
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'Text' ) ); ?>"><?php echo esc_html__( 'Text:', 'text_domain' ); ?></label>
<textarea class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'text' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'text' ) ); ?>" type="text" cols="30" rows="10"><?php echo esc_attr( $text ); ?></textarea>
</p>
<?php
}
public function update( $new_instance, $old_instance ) {
$instance = array();
$instance['title'] = ( ! empty( $new_instance['title'] ) ) ? strip_tags( $new_instance['title'] ) : '';
$instance['text'] = ( ! empty( $new_instance['text'] ) ) ? $new_instance['text'] : '';
return $instance;
}
}
$my_widget = new My_Widget();
- The WP_Widget class is located in wp-includes/class-wp-widget.php
Filesystem API
class WP_Filesystem_Base {}
Base WordPress Filesystem class which Filesystem implementations extend.
Filesystem API abstracts out the functionality needed for reading and writing local files to the filesystem to be done securely, on a variety of host types.
request_filesystem_credentials( $url, '', false, false, null )
The
request_filesystem_credentialscall will test to see if it is capable of writing to the local filesystem directly without credentials first. If this is the case, then it will return true and not do anything. Your code can then proceed to use theWP_Filesystemclass.The
request_filesystem_credentialscall also takes into account hardcoded information, such as hostname or username or password, which has been inserted into thewp-config.phpfile using defines. If these are pre-defined in that file, then this call will return that information instead of displaying a form, bypassing the form for the user.Instead of using raw PHP functions like
file_put_contents()orfopen(), WordPress provides this API so file operations work across different server environments (shared hosting, FTP, SSH, etc.) and respect WordPress security rules.
require_once ABSPATH . 'wp-admin/includes/file.php';
global $wp_filesystem;
if ( ! WP_Filesystem() ) {
return false;
}
# writing
$file = WP_CONTENT_DIR . '/test.txt';
$content = 'Hello WordPress';
$wp_filesystem->put_contents($file, $content, FS_CHMOD_FILE);
# read
$content = $wp_filesystem->get_contents($file);
# delete
$wp_filesystem->delete($file);
# extra
$upload_dir = wp_upload_dir();
$dir = $upload_dir['basedir'] . '/myplugin';
FS_CHMOD_FILE = 0644
FS_CHMOD_DIR = 0755
- Example of FS
- Class: WP_Filesystem_Direct
- Class: WP_Filesystem_FTPext
- Class: WP_Filesystem_ftpsocket
- Class: WP_Filesystem_SSH2
File Header API
- The File Header API does not load the entire file into memory. It reads only the first 8 KiB (8192 bytes) of the file.
- Reading a file:
# addons-pro.php
<?php
/*
* Addon Name: Pro Features
* Support Level: Premium
* Documentation ID: 12345
*/
# retrieval
$file_path = plugin_dir_path( __FILE__ ) . 'addon-pro.php';
$all_headers = array(
'name' => 'Addon Name',
'support' => 'Support Level',
'doc_id' => 'Documentation ID',
);
$data = get_file_data( $file_path, $all_headers );
echo $data['name']; // Outputs: Pro Features
echo $data['support']; // Outputs: Premium
- get_plugin_data() : Specifically for plugins. It looks for headers like Plugin Name, Plugin URI, Description, Version, Author, and License.
- wp_get_theme() is the modern way to access theme headers (from style.css). It returns a WP_Theme object.
Abilities API
The Abilities API (introduced in WordPress 6.9) represents a major shift in how WordPress exposes its functionality. While the traditional Roles and Capabilities API focuses on who can do something, the Abilities API focuses on what the site can actually do in a machine-readable format.
It is designed to be a “discovery layer” that allows external systems—specifically AI agents, automation tools, and other plugins—to understand exactly what functions are available, what data they require, and what they will return.
# register category
add_action( 'wp_abilities_api_categories_init', function() {
wp_register_ability_category( 'ecommerce-tools', array(
'label' => 'E-commerce Tools',
));
});
# register category
add_action( 'wp_abilities_api_init', function() {
wp_register_ability( 'shop/calculate-discount', array(
'label' => 'Calculate Discount',
'description' => 'Calculates the loyalty discount for a specific user.',
'category' => 'ecommerce-tools',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'user_id' => array( 'type' => 'integer' ),
),
'required' => array( 'user_id' ),
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'discount_percent' => array( 'type' => 'number' ),
),
),
'execute_callback' => 'my_calculate_discount_logic',
'permission_callback' => function() {
return current_user_can( 'manage_options' );
},
));
});
function my_calculate_discount_logic( $params ) {
$user_id = $params['user_id'];
// Logic to determine discount...
return array( 'discount_percent' => 15.5 );
}
- WP uses Requests library for HTTP requests. Can use cURL or PHP streams if available.
- Uses
WP_Httpclass WP_HTTP_Requests_Responsewrapper is used to convert underlying request/response to standardised format.- Unlike settings stored in
wp_options, the specific roles and individual user capabilities are stored in thewp_usermetatable under the meta keywp_capabilities.
wp_cache_add()
wp_cache_set()
delete
replace
Unlike WP-Cron, which stores tasks in a single database option (cron), Action Scheduler uses its own custom database tables. This makes it much faster and prevents your options table from bloating.
| Feature | WP-Cron | Action Scheduler |
|---|---|---|
| Storage | wp_options (serialized) | Custom DB Tables (indexed) |
| Traceability | None (it’s “fire and forget”) | Full logs for every task |
| Reliability | Can miss events if traffic is low | Highly robust; handles thousands of jobs |
| Concurrency | Single-threaded | Supports parallel processing batches |
// 1. Schedule the action
// as_schedule_single_action( $timestamp, $hook, $args, $group );
as_schedule_single_action( time() + DAY_IN_SECONDS, 'my_custom_followup_email', array( 'user_id' => 123 ) );
// 2. Hook the callback
add_action( 'my_custom_followup_email', 'handle_my_followup_email' );
function handle_my_followup_email( $user_id ) {
// Your logic here
error_log( "Sending email to user: " . $user_id );
}
// Schedules a task to run every hour starting now
if ( ! as_has_scheduled_action( 'my_daily_cleanup' ) ) {
as_schedule_recurring_action( time(), HOUR_IN_SECONDS, 'my_daily_cleanup' );
}
add_action( 'my_daily_cleanup', 'run_cleanup_logic' );
function run_cleanup_logic() {
// Perform cleanup
}
Object Cache API
// wp_cache_set( $key, $data, $group, $expire );
$user_data = array( 'name' => 'John', 'role' => 'Editor' );
wp_cache_set( 'user_profile_123', $user_data, 'my_plugin_users', 3600 ); // Cache for 1 hour
$cached_val = wp_cache_get( 'user_profile_123', 'my_plugin_users' );
if ( false === $cached_val ) {
// Cache Miss: Run the expensive DB query here
$cached_val = $wpdb->get_row( "SELECT * FROM ... WHERE id = 123" );
// Save it for next time
wp_cache_set( 'user_profile_123', $cached_val, 'my_plugin_users' );
}
return $cached_val;
wp_cache_delete( 'user_profile_123', 'my_plugin_users' );
Out of the box, WordPress uses a Non-Persistent object cache. This means the data is stored in PHP’s memory for that specific execution.
A file named object-cache.php placed in your /wp-content/ folder that tells WordPress to send “cache” data to Redis instead of just keeping it in PHP memory.
Set is an “Overwrite,” while Add is a “Protect.” add does not overwrite key if only present.
In the WordPress ecosystem, Drop-ins are specialized PHP files that live in the /wp-content/ directory (not in a plugin or theme folder). They allow you to “drop in” and replace or extend core WordPress functionality at a very low level—often before plugins or even the database are fully loaded.
| File Name | Purpose | When to use it |
|---|---|---|
object-cache.php | The Most Popular. Replaces the default Object Cache with a persistent one (Redis/Memcached). | High-traffic sites needing performance. |
advanced-cache.php | Used by page caching plugins (like WP Rocket or W3 Total Cache). | To enable full-page HTML caching. |
db.php | Replaces the standard $wpdb database class. | Connecting to external DBs or database clustering. |
maintenance.php | Customizes the “Briefly unavailable for scheduled maintenance” screen. | Custom branding during updates. |
db-error.php | Customizes the “Error establishing a database connection” screen. | Professional error pages for users. |
sunrise.php | Used in Multisite to handle domain mapping and site loading. | Advanced Multisite networks. |
The Two Primary Cookies
WordPress splits its cookies to balance security and functionality:
wordpress_[hash]:Purpose: Specifically for the
/wp-admin/area.Security: It is usually restricted to the admin path. If this cookie is stolen, the attacker has the “keys to the castle,” but it is harder to sniff because it’s only sent during admin requests.
wordpress_logged_in_[hash]:Purpose: This is the “Front-end” cookie.
Security: It tells WordPress, “This user is logged in as ‘John’.” WordPress uses this to show the Admin Bar on the front end or to pre-fill comment forms. It does not grant access to the admin dashboard on its own.
The [hash] isn’t random; it is a unique identifier based on your site’s URL.
Username
Expiration Time
A Session Token (This is a random string stored in the database to validate the cookie).
HMAC Signature: A cryptographic signature created using your
SECURE_AUTH_SALT(fromwp-config.php). This prevents users from “faking” a cookie.
When a request hits your site, the authentication happens very early (in wp-settings.php calling wp_get_current_user()):
Cookie Check: WordPress looks for the
wordpress_logged_in_...cookie.Parsing: It splits the cookie into Username, Expiration, and Token.
Expiration Check: If the expiration time in the cookie has passed, the user is logged out.
Database Validation: It looks up the User by Username. It then checks the
session_tokensmeta to see if the token from the cookie is still valid/active.Global User Setup: If everything matches, it populates the
$current_userglobal object.