How to develop a WordPress plugin

I am going to show here how to create your own WordPress plugin.

0. My dev environment:

  • OS: Linux
  • Editor: Atom editor + PHP debugger + PHPCS
  • Web-server: built-in PHP web-server (php -S localhost:3000)

I use Linux and Atom editor with PHP debugger and PHPCS enabled. But any code editor work.

For coding styling, I recommend to use the WordPress coding standards, but PSR is also acceptable. Try to not use your own code styling, especially if other people are going to work on your code.

For Windows, I recommend “Local by Flywheel”.

1. Have a WordPress instance for the plugin:

We need a running copy of WordPress. This will be a shell for our plugin.

Either install a brand new WordPress copy (recommended) or work on an existing project.

2. WP-CLI:

In this tutorial, we are going to use WP-CLI, the official command line tool for interacting with and managing your WordPress sites.

For install instructions visit:

3. Our first plugin:

Let’s create our my-plugin plugin with wp-cli. In the WordPress root folder, run:

wp scaffold plugin my-plugin
(out)Success: Created plugin files.
(out)Success: Created test files.

This command creates a my-plugin folder inside the wp-content/plugins folder:

tree wp-content/plugins/my-plugin
(out)├── bin
(out)│   └──
(out)├── Gruntfile.js
(out)├── my-plugin.php
(out)├── package.json
(out)├── phpunit.xml.dist
(out)├── readme.txt
(out)└── tests
(out)    ├── bootstrap.php
(out)    └── test-sample.php
(out)2 directories, 8 files

Go to Plugins and activate the plugin:

Now, open the my-plugin/my-plugin.php file:

And replace the // Your code ... with echo 'Hello, World!':

Now, open the home page:

Notice the "Hello, World!" on the top left. Open the source code and notice that it is the very first string of the code.

Congratulations! You have created a WordPress plugin!!!

4. Enable debug mode:

Set define('WP_DEBUG', true) on your wp-config.php (the constant is already defined, just change the value to true):

 * For developers: WordPress debugging mode.
 * Change this to true to enable the display of notices during development.
 * It is strongly recommended that plugin and theme developers use WP_DEBUG
 * in their development environments.
 * For information on other constants that can be used for debugging,
 * visit the Codex.
 * @link
define( 'WP_DEBUG', true );

/* That's all, stop editing! Happy publishing. */

/** Absolute path to the WordPress directory. */
if ( ! defined( 'ABSPATH' ) ) {
        define( 'ABSPATH', dirname( __FILE__ ) . '/' );

/** Sets up WordPress vars and included files. */
require_once( ABSPATH . 'wp-settings.php' );

The log is going to be saved to wp-content/debug.log

5. Change the headers:

Adapt the headers with your own information: plugin name, URL, version, etc.

Also, delete the "echo" command.

Save and reload the plugins page:

Text Domain and Domain Path are optional and used for internationalization (translation). If omitted, "Text Domain" is the plugin slug (ex. "my-plugin"). "Domain Path" is only necessary if your plugin is not in the WP official plugin repository. If not published there, leave the "/languages" set.

For an updated version of the header fields go to the official WP site:

6. Security: Prevent direct access to your file

Prevent direct access to your file with defined( 'ABSPATH' ) || exit;

7. The plugin class:

1. Next step is to:

  • Create a class ("MyPlugin");
  • Put initialization code in __construct();
  • Create an instance object;
  • Save the instance as a global variable (optional), so it can be accessed anywhere;
  • Do not create if the class already exists;

2. The resulting code:

8. (OPTIONAL) Do not use globals:

Saving the instance as a global object is not the best solution.

A better approach is to define a global my_plugin() function that returns the current instance. This function is just an alias to the "instance()" method, so we don't need to call "MyPlugin::instance()".

9. Hooks: filters and actions

Hooks are the way WordPress extends its functionality and are crucial for plugin development.

There are 2 hook types: actions and filters.

9.1. Actions:

To explain actions, let's use a real-world example. In WordPress, whenever the footer is rendered, an event called wp_footer happens. We can assign our own code to this event, just like in JS we can develop our own code for events (ex. the "onclick" event in JS).

Events are called "actions" in WordPress.

Now, let's use actions to change the footer. Put the following code in our constructor:

Now, open the homepage and check the footer:

When the footer is rendered, it calls a do_action('wp_footer'), which triggers all methods assigned to the "wp_footer" action.

do_action() vs add_action():

  • do_action(): Producer side. I.e. used by the side that defines a new action. Example: WordPress core;
  • add_action(): Consumer side. I.e. used by the side that consumes the action. Example: plugin;

Using methods instead of functions:

Notice that we have defined our my_fnc action as a function. Now, let's refactor it as a method:

When using a method on add_action(), pass the instance object ($this) and the method name in an array.

Static methods:

Just pass the class name instead of $this. Example:

class MyPlugin {
  public function __construct() {
    add_action( 'wp_footer', [ 'MyPlugin', 'my_fnc' ] );

  public static function my_fnc() {
    echo 'Hello World';

This is specially useful when calling methods from a different class.

Localization (multiple idioms, i18n):

To support multiple idioms, instead of echo 'Hello World', we should use the _e()  or echo __() function.

_e() is a synonym for echo __().

Text Domain:

The second parameter is your plugin namespace, aka text-domain. By default, the text domain is your plugin slug (your folder name, ex. my-plugin), but you can define a different text-domain on the header.

Variables, plurals and others:

For advanced usage, check the official documentation:

Even if you are not going to translate your plugin to multiple languages, it is a BEST PRACTICE to use __() and _e() for human-readable outputs.


Methods are called in the same order they have been added. Example:

This will show a "World Hello" (because fnc2 is being called before fnc1).

We can also manually specify the order with a 3rd argument - the priority. Lower numbers are executed earlier.

add_action( 'wp_footer', 'my_fnc2', 20 );
add_action( 'wp_footer', 'my_fnc1', 10 );

This will show a "Hello World".

If not specified, the default priority is 10.

Passing arguments:

Some actions require extra arguments. For example, let's say we want to do some cleanup when a post is deleted. To implement this, we should listen to the wp_trash_post action, which passes a $post_id argument:

add_action( 'wp_trash_post', my_trash_fnc, 10, 1 );
function my_trash_fnc( $post_id ) {
  // my cleanup code

The last argument 1, is the function argument count.

On WP core, this is executed whenever a post is deleted:

do_action( 'wp_trash_post', $post_id );

Multiple arguments:

If there more than one argument, you should pass them all in an array on do_action():

To consume this action, just define the arguments as usual and set the argument count in the "add_action()".

add_action( 'some_action', my_fnc, 10, 4 );
function my_fnc( $arg1, $arg2, $arg3, $arg4 ) {
  // code

Other core actions:

WordPress has many other actions that we can use to extend it.

The complete list:

Besides those, each plugin/theme can have its own actions which you can use to extend them. For example, Woocommerce has many of them:

Custom actions:

It is a good practice to add your own hooks to your code, so other developers can extend it.

You define a new action by calling a do_action('your_unique_name'). Then, document it, so other plugins will be able to extend your plugin.

9.2. Filters:

Filters are about getting variables. For example, there is a core filter named wp_title that gets the title of blog.

Let's say we want to replace the all the os in the blog's title with es. Just do this:

If the blog title is "Hello World", it will become "Helle Werld".

Your filter function should ALWAYS have a return

Defining a new filter:

The filter equivalent of do_action() is apply_filters().

Let's see how WP's core gets the title:

The second argument $title is the starting value. It can be also used as a default value, if no methods are added to the filter.


The syntax is the same as in actions:

Then, call it with (no need to pass multiple arguments in an array):

10. Debug: Viewing hooks:

A handy tool for WordPress development are plugins that show the hooks on screen.

I recommend the "Simply Show Hooks" plugin:

10.1. Usage:

After installing and activate it, you must be logged in as WordPress admin to use it.

Open any page > admin toolbar (the black, topmost bar) > Simply Show Hooks > Show action hooks

This ends part 1 of this tutorial. On Part 2 we will see how to create a menu item and present a form on the admin side.

Close Menu