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.
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:
Relevant functions include: has_action, add_action, do_action, do_action_ref_array, did_action, remove_action, remove_all_actions, doing_action.
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 );
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_filter, add_filter, apply_filters, apply_filters_ref_array, current_filter, remove_filter, remove_all_filters, doing_filter.
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: 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' );
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.
// 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.
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: http://danieliser.com/presentation/intro-wordpress-plugin-development/ Description: Basic WordPress Plugin Header Comment Version: 1.0.0 Author: Daniel Iser Author URI: http://danieliser.com/ License: GPL2 License URI: https://www.gnu.org/licenses/gpl-2.0.html Text Domain: my-custom-plugin Domain Path: /languages */
If you want to host your Plugin on https://wordpress.org/plugins/, you need to create a readme.txt file within your plugin directory in a standardized format: Sample Readme, Readme 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: https://www.gnu.org/licenses/gpl-2.0.html 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).
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:
/assets /scripts /styles /images /includes /classes /languages /templates /my-custom-plugin.php
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.
/** * 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 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: https://developer.wordpress.org/reference/functions/register_post_type/
/** * Register a custom genre taxonomy for the book post type. */ function mcp_custom_taxonomy() { register_taxonomy( 'genre', array( 'book' ), // string|array of post types. array( 'label' => __( 'Genre', 'my-custom-plugin' ), 'rewrite' => array( 'slug' => 'genre' ), 'hierarchical' => true, ) ); } add_action( 'init', 'mcp_custom_taxonomy' );
For more info: https://developer.wordpress.org/reference/functions/register_taxonomy/
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.
/** * 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( array( 'taxonomy' => 'genre', 'field' => 'slug', 'terms' => 'fiction', ), ), ) ); return new WP_Query( $args ); }
/** * 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( $wpdb->prepare( "SELECT * FROM $wpdb->book_sales WHERE author_id = %d AND total_sales > '%s'", $author_id, $decent_sales ) ); return $results; }
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' ); wp_enqueue_script( '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' );
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 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 ) ); }
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;
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; }