Main plugin file
The main plugin file should not be the place where all the logic lives, but an entry point.
Its task is to safely start the plugin, check the environment, and hand control to a properly structured code base.
A good main file usually does only this:
- contains plugin headings
- forbids direct execution of the file
- defines base constants
- loads autoloading or core files
- registers activation/deactivation hooks
- runs the plugin at the right moment (usually on the plugins_loaded hook)
- does not contain business logic
Example:
<?php
/**
* Plugin Name: My Plugin
* Description: Short plugin description.
*
* Requires at least: 6.5
* Requires PHP: 8.4
*
* Author: Author Name
* Author URI: https://example.com
* License: GPL-2.0+
* Text Domain: my-plugin
*
* Version: 1.0.0
*/
defined( 'ABSPATH' ) || exit;
define( 'MY_PLUGIN_FILE', __FILE__ );
define( 'MY_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'MY_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'MY_PLUGIN_VERSION', '1.0.0' );
require_once MY_PLUGIN_DIR . 'vendor/autoload.php';
register_activation_hook( __FILE__, [ My_Plugin\Activator::class, 'activate' ] );
register_deactivation_hook( __FILE__, [ My_Plugin\Deactivator::class, 'deactivate' ] );
add_action( 'plugins_loaded', 'my_plugin_bootstrap' );
function my_plugin_bootstrap() {
if ( ! my_plugin_requirements_met() ) {
return;
}
My_Plugin\Plugin::instance()->init();
}
function my_plugin_requirements_met() {
return version_compare( PHP_VERSION, '8.1', '>=' );
}
The principle: the file is a declaration, not an implementation
The most correct logic is: the less code in the main file, the better. Everything that can be moved out of it without losing clarity should be moved out. Its goal is not to implement the plugin, but to safely and clearly start it.
A bad main file is one where CPTs, shortcodes, AJAX, REST API, admin, settings, cron, styles, scripts are registered right away. Hundreds of lines of code, hooks mixed with functions, global variables... After a year, such a file becomes a dump.
It should not know the implementation details. It only prepares the environment and starts the main plugin class.
The correct order is:
- WordPress loads the plugin file.
- The file checks whether it can run.
- Autoloading is loaded.
- Lifecycle hooks are registered.
- On
plugins_loadedorinitthe main class runs. - Beyond that, all logic lives inside separate classes/modules.
Why not run everything immediately when connecting the file?
Because at the moment of loading the main file WordPress may not be fully ready yet. Other plugins may not be loaded, translations may still be unavailable, some APIs may be too early to use. Therefore the main file should only prepare, and the real launch is better done via a hook.
For a small plugin you can manage with functions. For a medium and large one it’s better to immediately create a separate class Plugin, which registers the other parts:
namespace My_Plugin;
class Plugin {
public static function instance(): self {
static $instance;
if ( ! $instance ) {
$instance = new self();
}
return $instance;
}
private function __construct() {
$this->define_constants(); // if not moved to the main file
$this->load_dependencies();
$this->set_locale();
$this->define_hooks();
}
public function init(): void {
( new Assets() )->init();
( new Admin() )->init();
( new Rest_Api() )->init();
}
}
Is Singleton mandatory
Singleton is often used for the main plugin class, but it is not mandatory and not always the best option.
I don’t like it much. Its downside is that the class creates itself and stores its own instance. Because of this it’s harder to test, harder to reinitialize with different parameters, and the class gets extra responsibility (SRP violated) — it is responsible not only for the plugin’s work but for its own creation.
For me, a softer approach is to create the plugin object in the main file and pass it the necessary parameters.
If quick access to the plugin object is needed, you can create a helper function plugin(). However it’s better not to use it inside business logic, and to pass dependencies explicitly.
Example:
Main.php
<?php
/**
* Plugin Name: My Plugin
* Description: Short plugin description.
*
* Requires at least: 6.5
* Requires PHP: 8.4
*
* Author: Author Name
* Author URI: https://example.com
* License: GPL-2.0+
* Text Domain: my-plugin
*
* Version: 1.0.0
*/
namespace My_Plugin;
defined( 'ABSPATH' ) || exit;
require_once __DIR__ . '/vendor/autoload.php';
register_activation_hook( __FILE__, [ Activator::class, 'activate' ] );
register_deactivation_hook( __FILE__, [ Deactivator::class, 'deactivate' ] );
add_action( 'plugins_loaded', '\My_Plugin\load' );
function load(): void {
plugin()->init();
}
function plugin(): Plugin {
static $plugin = null;
$plugin ??= new Plugin( __FILE__ );
return $plugin;
}
Plugin.php
<?php
namespace My_Plugin;
class Plugin {
public readonly string $main_file;
public readonly string $dir; // no end slash
public readonly string $url; // no end slash
public readonly string $ver;
public readonly string $name;
public readonly string $desc;
public function __construct( string $main_file ) {
$file_data = get_file_data( $main_file, [
'ver' => 'Version',
'name' => 'Name',
'desc' => 'Description',
] );
$this->main_file = $main_file;
$this->dir = dirname( $main_file );
$this->url = plugins_url( '', $main_file );
$this->ver = $file_data['ver'];
$this->name = $file_data['name'];
$this->desc = $file_data['desc'];
}
public function init(): void {
( new Assets( $this->ver ) )->init();
( new Admin( $this->main_file ) )->init();
( new Rest_Api() )->init();
}
}