Главный файл плагина

Главный файл плагина должен быть не местом, где живет вся логика, а точкой входа.

Его задача - аккуратно запустить плагин, проверить окружение и передать управление нормальной структуре кода.

Хороший главный файл обычно делает только это:

  • содержит заголовоки плагина
  • запрещает прямой запуск файла
  • определяет базовые константы
  • подключает автозагрузку или основные файлы
  • регистрирует хуки активации/деактивации
  • запускает плагин в правильный момент (обычно на хуке plugins_loaded).
  • не содержит бизнес-логику

Пример:

<?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', '>=' );
}

Главный принцип: файл - это декларация, не реализация

Самая правильная логика такая: чем меньше кода в главном файле, тем лучше. Все, что можно вынести из него без потери понятности, нужно выносить. Его цель - не реализовывать плагин, а безопасно и понятно его запустить.

Плохой главный файл - это когда в нем сразу регистрируются CPT, шорткоды, AJAX, REST API, админка, настройки, крон, стили, скрипты. Сотни строк кода, хуки вперемешку с функциями, глобальные переменные... Через год такой файл превращается в свалку.

Он не должен знать детали реализации. Он только подготавливает окружение и запускает главный класс плагина.

Правильный порядок такой:

  1. WordPress загружает файл плагина.
  2. Файл проверяет, можно ли запускаться.
  3. Подключается автозагрузка.
  4. Регистрируются lifecycle-хуки.
  5. На plugins_loaded или init запускается основной класс.
  6. Дальше вся логика живет внутри отдельных классов/модулей.

Почему не стоит запускать все сразу при подключении файла?

Потому что в момент загрузки главного файла WordPress еще может быть не полностью готов. Другие плагины могут быть не загружены, переводы еще могут быть недоступны, некоторые API еще рано использовать. Поэтому главный файл должен только подготовиться, а реальный запуск лучше делать через хук.

Для маленького плагина можно обойтись функциями. Для среднего и большого лучше сразу делать отдельный класс Plugin, который регистрирует остальные части:

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(); // если не вынесли в главный файл
		$this->load_dependencies();
		$this->set_locale();
		$this->define_hooks();
	}

	public function init(): void {
		( new Assets() )->init();
		( new Admin() )->init();
		( new Rest_Api() )->init();
	}
}

Обязательно ли Singleton

Singleton часто используют для главного класса плагина, но это не обязательный и не всегда лучший вариант.

Мне не очень нравится. Его минус в том, что класс сам себя создает и хранит собственный экземпляр. Из-за этого его сложнее тестировать, сложнее переинициализировать с другими параметрами, а сам класс получает лишнюю ответственность (SRP нарушен) - он отвечает не только за работу плагина, но и за свое создание.

Для меня более мягкий подход - создать объект плагина в главном файле и передать ему нужные параметры.

Если нужен быстрый доступ к объекту плагина, можно сделать helper-функцию plugin(), Однако лучше не использовать её внутри бизнес-логики, а передавать зависимости явно.

Пример:

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();
	}
}