Intro to WordPress Plugin Development


Daniel Iser

What we are covering

  • WordPress Plugin API
  • Creating a Plugin
  • Plugin Examples

WordPress Plugin API

  • Hooks – Actions & Filters
  • Posts, Taxonomies & Meta
  • Options
  • Internationalization
  • Security


Hooks are provided by WordPress and other developers to allow you to “hook into” the flow of things. This allows you to call functions in your plugin at specific times making it only run when needed.

WordPress has 2 types of hooks: Actions & Filters.

Hooks – Actions

Actions are triggered by specific events that take place in WordPress, such as publishing a post, registering a user, or displaying an a page. An Action is a function defined in your plugin and hooked to some of these events. Actions usually do one or more of the following:

  • Modify database data.
  • Send notifications or log information.
  • Modify the generated page before it is sent to a users browser.

Relevant functions include: has_action, add_action, do_action, do_action_ref_array, did_action, remove_action, remove_all_actions, doing_action.

Hooks – Using Actions

Adding actions;

function my_custom_action_function ( $arg1, $arg2 ) {
    // Render additional html or complete some process when the action is triggered.
add_action( 'my_custom_action', 'my_custom_action_function', 10, 2 );

Triggering an action:

do_action( 'my_custom_action', $arg1, $arg2 );

Hooks – Filters

Filters are functions that WordPress passes data through, at certain points in execution, just before taking some action with the data (such as adding it to the database or sending it to the browser screen). Filters sit between the database and the browser (when WordPress is generating pages), and between the browser and the database (when WordPress is adding new posts and comments to the database); most input and output in WordPress passes through at least one filter. WordPress does some filtering by default, and your plugin can add its own filtering.

Relevant functions include: has_filteradd_filter, apply_filters, apply_filters_ref_array, current_filter, remove_filter, remove_all_filters, doing_filter.

Hooks – Using Filters

Adding filters.

function my_custom_filter_function( $content, $arg1, $arg2 ) {
    // Modify the content here if needed then return it for other filters.
    return $content;
add_filter( 'my_custom_filter', 'my_custom_filter_function', 10, 3 );

Using filters:

$content = 'Some Content';
$content = apply_filters( 'my_custom_action', $content, $arg1, $arg2 );

Posts, Taxonomies & Meta

Posts: WordPress can hold and display many different types of content. A single item of such a content is generally called a post, although post is also a specific post type. Internally, all the post types are stored in the same place, in the wp_posts database table, but are differentiated by a column called post_type. Post types can include: posts, pages, products, popups etc.

Taxonomies: WordPress uses taxonomies to categorize and group post content together. These could include tags, categories or custom such as brand or manufacturer for product content types.

Meta: All of WordPress’s various object types (post, taxonomy & user) allow for meta data storage as well. This can be used to store non standard or custom data for  objects. You can also query for objects based on their stored meta. You might store SEO information, display settings etc for your posts & pages.



WordPress provides a built in api for handling storage of options for the site. There are 3 functions that will handle nearly everything you need in terms of storage: add_option, update_option, get_option & delete_option.

Just like the meta api data will be serialized & unserialized automatically. So you can pass in full arrays.

// Get an option, allows passing in a default as the second argument.
$my_option = get_option( 'my_custom_option', false );
// Insert an option if it doesn't exist.
add_option( 'my_custom_option', $my_option );
// Update or Add an option.
update_option( 'my_custom_option', $my_option );
// Delete an option
delete_option( 'my_custom_option' );


Internationalization & Translation

What is I18n? Internationalization is the process of developing your plugin so it can easily be translated into other languages. Localization describes the subsequent process of translating an internationalized plugin. Internationalization is often abbreviated as i18n (because there are 18 letters between the i and the n) and localization is abbreviated as l10n (because there are 10 letters between the l and the n.).

WordPress uses the gettext libraries and tools for i18n. There are a few main functions you will need to use for any text that needs to be translated: __(), _e(), _n(). There are quite a few more for various other purposes such as adding context to your strings. Check out I18n for WordPress Developers for more info.

Each plugin & theme should have a unique text-domain. This will be passed to all of the I18n functions to prevent translations from being confused from one plugin to another.

I18n Usage

// Translation into variable.
$string = __( 'Translated Text', 'my-custom-plugin' );

// Echo translation immediately.
_e( 'Translated Text', 'my-custom-plugin' );

// Translation with context.
$string = _x( 'Translated Text', 'Page title', 'my-custom-plugin' );

// Translation of plurals.
$count = 2;
printf( esc_html( _n( '%d item', '%d items.', $count, 'my-custom-plugin' ) ), $count );

// Escaping html with dynamic replacement.
printf( esc_html__( 'Your city is %1$s, and your zip code is %2$s.', 'my-text-domain' ), $city, $zipcode );



WordPress is at its core a secure platform, but often due to unsecure code in plugins or themes a users site can become compromised. WordPress offers a variety of security functions to keep your code & users safe.

  • Validation – These are the checks that are run to ensure the data you have is what it should be. For instance, that an e-mail looks like an e-mail address, that a date is a date and that a number is (or is cast as) an integer.
  • Sanitization / Escaping – These are the filters that are applied to data to make it ‘safe’ in a specific context. For instance, to display HTML code in a text area it would be necessary to replace all the HTML tags by their entity equivalents.
  • Nonces – Used to prevent CSRF attacks.

Creating a Plugin

  • Plugin header
  • Plugin Readme
  • Quick start with Pluginception
  • Organization
  • OOP, Namespacing, Autoloading, Build Routines?

Plugin Header

Every plugin has a single file entry point. WordPress standards assume the file name will match the folder slug. The plugins text-domain should generally match this slug as well. At the top of your plugins main file should begin with this header block, which WordPress uses to identify your plugin.

<?php // In our examples it will be `wp-content/plugins/my-custom-plugin/my-custom-plugin.php`.
Plugin Name: My Custom Plugin
Plugin URI:
Description: Basic WordPress Plugin Header Comment
Version:     1.0.0
Author:      Daniel Iser
Author URI:
License:     GPL2
License URI:
Text Domain: my-custom-plugin
Domain Path: /languages

Plugin Readme

If you want to host your Plugin on, you need to create a readme.txt file within your plugin directory in a standardized format: Sample ReadmeReadme Generator.

The WordPress plugin repository uses the “Requires” and “Tested up to” versions from the readme.txt in the stable tag.

=== My Custom Plugin ===
Contributors: danieliser
Donate link: 
Tags: custom plugin
Requires at least: 4.6
Tested up to: 4.7.2
Stable tag: 1.0.0
License: GPLv2 or later
License URI:

Here is a short description of the plugin.  This should be no more than 150 characters.  No markup here.

== Description ==

This is the long description.  No limit, and you can use Markdown (as well as in the following sections).


Quick Start with Pluginception

Thanks to @Otto42 there is an awesome plugin called Pluginception that can speed up initial creation of a plugin. Fill in a few fields and click the button and you instantly have a new plugin created and are can start making changes right away.


How you organize your code is completely up to you, but it is generally best to do it in a logical way. This way any developer that works on it after you can easily find what they need.

My typical folder structure:


OOP, Namespacing, Autoloading, Build Routines?

Absolutely. How far you go may depend on your target audience. If you want maximum compatibility you would shy away from namespacing and [] array assignments. WP core currently support PHP 5.2.4+ but recommends PHP 7. This means there are users everywhere between. If you want to develop with PHP 5.4+ only that is perfectly fine, just be sure in your plugins initialization code that you check for older versions and fail safely & quietly so that it doesn’t break a users site upon install.

  • Fail Gracefully
  • If you use build routines you will want to be sure to clean up before releasing.
  • Add polyfills when you can i.e. spl_autoload_register

Plugin Examples

  • Content Filtering
  • Register a Post Type
  • Register a Taxonomy
  • Database Queries
  • Scripts & Styles
  • Rest API
  • AJAX
  • Settings

Filtering the_content

 * Add featured image before the $content.
 * @param string $content
 * @return string
function mcp_featured_image_before_content( $content ) {
    if ( is_singular( 'post' ) && has_post_thumbnail() ) {
        $thumbnail = get_the_post_thumbnail();

        $content = $thumbnail . $content;

    return $content;

add_filter( 'the_content', 'mcp_featured_image_before_content' );


Register a Post Type

 * Register a book post type.
function mcp_custom_post_type() {
	$args = array(
		'public' => true,
		'label'  => __( 'Books', 'textdomain' ),
	register_post_type( 'book', $args );
add_action( 'init', 'mcp_custom_post_type' );

For more info:

Register a Taxonomy

 * Register a custom genre taxonomy for the book post type.
function mcp_custom_taxonomy() {
        array( 'book' ), // string|array of post types.
            'label' => __( 'Genre', 'my-custom-plugin' ),
            'rewrite' => array( 'slug' => 'genre' ),
            'hierarchical' => true,
add_action( 'init', 'mcp_custom_taxonomy' );

For more info:

Database Queries

WordPress provides multiple ways to query the database depending on your needs. If you are querying for content use one of the built in WP_Query, WP_Tax_Query or WP_User_Query. Otherwise you can run basic or custom queries using the global $wpdb object.

The object query classes provide many benefits, including shortcuts for complex SQL using simple array based arguments, easy pagination, automatic caching & meta loading; not to mention easy loop handling.

If you need raw sql power or to work with custom tables you will want to get familiar with the $wpdb object which includes many useful methods including ->insert(), ->update() & ->delete() as well as ->prepare() & ->query() for running custom queries.

Object Queries

 * Get the latest fiction books
 * @return WP_Query
function mcp_get_latest_fiction_books() {
    $args = apply_filters( 'mcp_get_latest_fiction_book_args', array(
        'post_type' => 'book',
        'posts_per_page' => 5,
        'tax_query' => array(
                'taxonomy' => 'genre',
                'field'    => 'slug',
                'terms'    => 'fiction',
    ) );

    return new WP_Query( $args );


Custom Queries

 * Get list of authors books with more than $decent_sales.
 * @param int $author_id
 * @param float $decent_sales
 * @return false|int
function mcp_get_author_book_with_decent_sales( $author_id = 0, $decent_sales = 500.00 ) {
    global $wpdb;

    $results = $wpdb->query(
            "SELECT * FROM $wpdb->book_sales WHERE author_id = %d AND total_sales > '%s'",

    return $results;

Assets – CSS & JavaScript

WordPress has built in APIs to handle assets & dependency management. Failure to use this API to load your scripts & styles will likely lead to headaches, broken scripts & unpredictable style behaviors.

 * Enqueue our custom scripts & styles.
function mcp_register_scripts_styles() {
    wp_enqueue_style( 'my-custom-plugin', MCP_URL . 'assets/styles/my-custom-plugin.css', null, '1.0.0' );

        'my-custom-plugin', // Assets handle
        MCP_URL . 'assets/scripts/my-custom-plugin.js', // Assets url
        array( 'jquery' ), // Array of dependencies
        '1.0.0', // Asset Version.
        true // Load JS in footer.
add_action( 'wp_enqueue_scripts', 'mcp_register_scripts_styles' );

Rest API Endpoints

WordPress now includes a complete REST API which is simple to extend with custom endpoints.

 * Register REST API endpoints
function mcp_rest_api_routes() {
    register_rest_route( 'mcp/v1', '/author/(?P<id>\d+)', array(
        'methods' => 'GET',
        'callback' => 'mcp_author_rest_callback',
        'args' => array(
            'id' => array(
                'validate_callback' => function($param, $request, $key) {
                    return is_numeric( $param );
    ) );
add_action( 'rest_api_init', 'mcp_rest_api_routes' );


Rest API Callbacks

Rest API callbacks accept the raw WP_REST_Request object which includes sanitized arguments and returns either a WP_Error or the data to be printed as a response.

 * Callback for rest api endpoint.
 * @param WP_REST_Request $request
 * @return array|WP_Error
function mcp_author_rest_callback( WP_REST_Request $request ) {
    $author_id = $request->get_param( 'id' );

    if ( ! $author_id ) {
        return new WP_Error( 'missing_api_arg', __( 'Missing author ID', 'my-custom-plugin' ) );

    return get_posts( array( 'post_type' => 'book', 'author' => $author_id ) );

AJAX Basics

WordPress uses 2 hooks for ajax listeners. One applies if users are logged in, the other for logged out users.

add_action( 'wp_ajax_my_custom_action', 'my_custom_action' );
add_action( 'wp_ajax_nopriv_my_custom_action', 'my_custom_action' );

To access these via your JS first you need the correct url. Adding this with your script registration will allow you to get the url from mcp_vars.ajaxurl.

/** Localize variables. */
wp_localize_script( 'my-custom-plugin', 'mcp_vars', array(
    'ajaxurl' => admin_url( 'admin-ajax.php' ),
) );

Lastly your ajax request data needs to include an action: ‘my_custom_action’ parameter which tells WordPress what action to trigger, this matches your add_action calls above.


If you need to manage user settings for your plugin the best option is usually storing an array in wp_options.

// Store our options array.
update_option( 'mcp_options', array( 'books_per_page' => 5 ) );

// Get the options array
$mcp_options = get_option( 'mcp_options', array() );

// Set the limit based on array values.
$limit = isset( $mcp_options['books_per_page'] ) ? $mcp_options['books_per_page'] : 5;


Settings Helper Functions

This wrapper function returns individual option keys, get_option is cached, so it is safe to call repeatedly.

 * Get an option from stored settings array.
 * @param $key
 * @param bool $default
 * @return bool
function mcp_get_option( $key, $default = false ) {
    $options = get_option( 'mcp_options', false );

    if ( ! $options ) {
        $options = mcp_default_options();
        update_option( 'mcp_options', $options );

    return isset( $options[ $key ] ) ? $options[ $key ] : $default;


Scroll To Top