A WordPress plugin is a PHP file (or a folder of files) placed in wp-content/plugins/. The only hard requirement is the plugin header comment in the main file. Everything else — hooks, classes, assets — is up to you.
Problem: What is the minimal file structure needed to create a valid WordPress plugin, and how do you organise it to avoid polluting the global namespace?
Solution: A plugin needs one PHP file in wp-content/plugins/ with a plugin header comment. Wrap all logic in a class instantiated after plugins_loaded — this keeps your code isolated and prevents function name collisions.
A minimal plugin file with a proper header and OOP structure:
<?php
/**
* Plugin Name: My Plugin
* Plugin URI: https://example.com/my-plugin
* Description: A brief description of what the plugin does.
* Version: 1.0.0
* Author: Your Name
* Author URI: https://example.com
* License: GPL-2.0+
* Text Domain: my-plugin
* Domain Path: /languages
*/
// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'MY_PLUGIN_VERSION', '1.0.0' );
define( 'MY_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'MY_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
class My_Plugin {
public function __construct() {
add_action( 'init', [ $this, 'load_textdomain' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] );
}
public function load_textdomain() {
load_plugin_textdomain( 'my-plugin', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
}
public function enqueue_assets() {
wp_enqueue_style( 'my-plugin', MY_PLUGIN_URL . 'css/style.css', [], MY_PLUGIN_VERSION );
}
}
new My_Plugin();
The activation and deactivation hooks are common places for setup and cleanup:
register_activation_hook( __FILE__, function() {
// create DB tables, add options, flush rewrite rules
flush_rewrite_rules();
} );
register_deactivation_hook( __FILE__, function() {
// clear scheduled events, flush rewrite rules
flush_rewrite_rules();
} );
register_uninstall_hook( __FILE__, 'my_plugin_uninstall' );
function my_plugin_uninstall() {
// delete options and custom tables
delete_option( 'my_plugin_settings' );
}
NOTE: The if ( ! defined( 'ABSPATH' ) ) exit; guard is essential — it prevents the file from being accessed directly via a URL. Always use plugin_dir_path() and plugin_dir_url() for paths and URLs rather than hardcoding them.