Skip to content

PHP Tutorial PHP for WordPress Developers

Creating a Simple Plugin

Lesson 333 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

A plugin is a PHP file (or folder) in wp-content/plugins/. WordPress finds it by a header comment and runs it when activated.

The smallest plugin

Create wp-content/plugins/reading-time/reading-time.php:

reading-time.php
PHP
<?php
/**
 * Plugin Name: Reading Time
 * Description: Adds an estimated reading time above single posts.
 * Version: 1.0.0
 * Requires PHP: 8.1
 * Author: Your Name
 * License: GPL-2.0-or-later
 * Text Domain: reading-time
 */

declare(strict_types=1);

if (!defined("ABSPATH")) {
    exit;   // do not run if the file is opened directly
}

function reading_time_minutes(string $content, int $wordsPerMinute = 200): int
{
    $words = str_word_count(wp_strip_all_tags($content));

    return max(1, (int) ceil($words / $wordsPerMinute));
}

add_filter("the_content", function (string $content): string {
    if (!is_singular("post") || !in_the_loop() || !is_main_query()) {
        return $content;
    }

    $minutes = reading_time_minutes($content);

    return sprintf('<p class="reading-time">%s</p>', esc_html(sprintf(_n("%d minute read", "%d minutes read", $minutes, "reading-time"), $minutes))) . $content;
});

Go to Plugins in the admin and activate it. Every single post now shows its reading time.

The parts

  1. Header comment. Only Plugin Name is required. The rest is information.
  2. ABSPATH check. Stops direct access to the file by URL.
  3. A prefixed function. reading_time_minutes(), so it never clashes with another plugin.
  4. A filter. The plugin changes data through a hook, never by editing core.
  5. Escaping and translation. esc_html(), _n() (singular and plural) and a text domain.

Testing the logic outside WordPress

The core calculation is plain PHP. We can run it directly.

index.php
PHP
<?php
function reading_time_minutes(string $text, int $wordsPerMinute = 200): int
{
    $words = str_word_count(strip_tags($text));

    return max(1, (int) ceil($words / $wordsPerMinute));
}

echo reading_time_minutes(str_repeat("word ", 450)) . " minutes\n";
echo reading_time_minutes("A short note") . " minute";
Output
OUTPUT
3 minutes
1 minute

Keep your logic in functions that do not depend on WordPress, and it is easy to test.

A class-based structure

For anything beyond a few lines, use a class and a namespace.

reading-time.php
PHP
<?php
/**
 * Plugin Name: Reading Time
 * Version: 1.0.0
 * Requires PHP: 8.1
 */

declare(strict_types=1);

namespace MyPlugin\ReadingTime;

if (!defined("ABSPATH")) {
    exit;
}

final class Plugin
{
    public function __construct(private int $wordsPerMinute = 200) {}

    public function register(): void
    {
        add_filter("the_content", [$this, "prepend"]);
    }

    public function minutes(string $content): int
    {
        return max(1, (int) ceil(str_word_count(wp_strip_all_tags($content)) / $this->wordsPerMinute));
    }

    public function prepend(string $content): string
    {
        if (!is_singular("post") || !in_the_loop() || !is_main_query()) {
            return $content;
        }

        $minutes = $this->minutes($content);

        return '<p class="reading-time">' . esc_html((string) $minutes) . " min read</p>" . $content;
    }
}

(new Plugin())->register();

Activation and deactivation

index.php
PHP
<?php
register_activation_hook(__FILE__, function (): void {
    add_option("reading_time_wpm", 200);
});

register_deactivation_hook(__FILE__, function (): void {
    // clean up scheduled tasks; do NOT delete user data here
});

Deleting data belongs in an uninstall step (uninstall.php in the plugin folder), which runs only when the user deletes the plugin.

Good practice checklist

  • A unique prefix or namespace for everything.
  • The ABSPATH guard in every PHP file.
  • Escape output, sanitise input, check nonces and capabilities.
  • Use hooks, never edit core or other plugins.
  • Load scripts only where needed (wp_enqueue_scripts with conditions).
  • Make text translatable (__(), _e(), a text domain).
  • Provide an uninstall routine, and do not leave junk in the database.
  • Follow the WordPress coding standards, but modern PHP (types, classes) is welcome in your own code.

Composer in plugins

You can use Composer inside a plugin for autoloading and libraries. Be careful: two plugins that bundle different versions of the same library can clash. Tools such as PHP-Scoper prefix the libraries to avoid it.

info
Learn by reading
Look inside popular plugins (the WordPress.org plugin directory shows the code). You now know enough PHP to follow them.