Лучшие практики для главного файла плагина

Лучшие практики для главного файла плагина

После жаркой дискуссии о том, как должен выглядеть главный файл плагина, внутри твита от Mark Jaquith , я решил написать вариант, с большинством пунктов я согласен, но об этом позже.

На начало дисскуссии у меня был готов набросок бойлерплейта для плагинов WordPress.

Программный код главного файла plugin-name.php плагина выглядел так:

<?php// Comments for plugin.use PluginName\Plugin;( new Plugin())->run();

а главный класс PluginName\Plugin так:

<?phpnamespace PluginName;use PluginName\Admin\Settings;use PluginName\Front\Front;class Plugin {public function run(): void {is_admin()? $this->run_admin(): $this->run_front();}private function run_admin(): void {( new Settings())->hooks();}private function run_front(): void {( new Front())->hooks();}}

Чтобы избавиться от Hard Dependencies в классе PluginName\Plugin, используем Dependency Injection Container(DIC).

Dependency Injection Container

Устанавливаем DIC и настраиваем его:

composer require symfony/dependency-injectioncomposer require symfony/config

Создаем файл конфигурации dependencies/services.php(но вы также можете без проблем использовать yml или xml формат):

<?phpuse Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;// Exit if accessed directly.if(! defined( 'ABSPATH')) {exit;}return function( ContainerConfigurator $configurator) {$services = $configurator->services();$services->set( 'settings', 'PluginName\Admin\Settings');$services->set( 'front', 'PluginName\Front\Front');};

Проще говоря, для каждого класса плагина создали уникальный слаг. Для PluginName\Admin\Settingssettings, а для PluginName\Front\Frontfront.

Кроме того в этом конфиге можно без труда указывать объекты, которые необходимо передать в конструктор или любой другой способ.

Для создания DIC и подключения файла-конфигруации нам надо написать следующий код:

<?phpuse Symfony\Component\Config\FileLocator;use Symfony\Component\DependencyInjection\ContainerBuilder;use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;require_once __DIR__. '/vendor/autoload.php';$container_builder = new ContainerBuilder();$loader = new PhpFileLoader( $container_builder, new FileLocator( __DIR__));$loader->load( PLUGIN_NAME_PATH. 'dependencies/services.php');

Обновим главный файл plugin-name.php и передадим DIC в коструктор объекта PluginName\Plugin:

<?phpuse PluginName\Plugin;use Symfony\Component\Config\FileLocator;use Symfony\Component\DependencyInjection\ContainerBuilder;use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;define( 'PLUGIN_NAME_PATH', plugin_dir_path( __FILE__));require_once PLUGIN_NAME_PATH. 'vendor/autoload.php';$container_builder = new ContainerBuilder();$loader = new PhpFileLoader( $container_builder, new FileLocator( __DIR__));$loader->load( PLUGIN_NAME_PATH. 'dependencies/services.php');$plugin_name = new Plugin( $container_builder);$plugin_name->run();

Давайте обновим класс PluginName\Plugin с использованием DIC:

<?phpnamespace PluginName;use Exception;use Symfony\Component\DependencyInjection\ContainerBuilder;class Plugin {private $container_builder;public function __construct( ContainerBuilder $container_builder ) {$this->container_builder = $container_builder;}public function run(): void {is_admin()? $this->run_admin(): $this->run_front();}private function run_admin(): void {$this->container_builder->get_service( 'settings' )->hooks();}private function run_front(): void {$this->container_builder->get_service( 'front' )->hooks();}}

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

Запуск плагина на событие plugins_loaded

Отложим запуск плагина до события plugins_loaded . Для этого создаем возможность run_plugin_name в которую оборачиваем весь вызов плагина и добавляем ее на событие plugins_loaded :

<?phpuse PluginName\Plugin;use Symfony\Component\Config\FileLocator;use Symfony\Component\DependencyInjection\ContainerBuilder;use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;define( 'PLUGIN_NAME_PATH', plugin_dir_path( __FILE__ ) );add_action( 'plugins_loaded', 'run_plugin_name' );function run_plugin_name() {require_once PLUGIN_NAME_PATH . 'vendor/autoload.php';$container_builder = new ContainerBuilder();$loader         = new PhpFileLoader( $container_builder, new FileLocator( __DIR__ ) );$loader->load( PLUGIN_NAME_PATH . 'dependencies/services.php' );$plugin_name = new Plugin( $container_builder );$plugin_name->run();}

Данный трюк позволяем включить/выключить плагин при помощи всего одного remove_action . Это полезно к примеру при вызове AJAX/REST API/WP CLI. Ситуации бывают различные, но мы даем такую функция без проблем управлять плагином в коде темы/иных плагинов.

Хук запуска плагина

Идем дальше. И добавляем событие plugin_name_init после запуска плагина:

<?phpuse PluginName\Plugin;use Symfony\Component\Config\FileLocator;use Symfony\Component\DependencyInjection\ContainerBuilder;use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;define( 'PLUGIN_NAME_PATH', plugin_dir_path( __FILE__ ) );function run_plugin_name() {require_once PLUGIN_NAME_PATH . 'vendor/autoload.php';$container_builder = new ContainerBuilder();$loader            = new PhpFileLoader( $container_builder, new FileLocator( __DIR__ ) );$loader->load( PLUGIN_NAME_PATH . 'dependencies/services.php' );$plugin_name = new Plugin( $container_builder );$plugin_name->run();do_action( 'plugin_name_init', $plugin_name );}add_action( 'plugins_loaded', 'run_plugin_name' );

В момент события plugin_name_init у нас есть объект главного класса плагина и в полном объеме добавленные хуки всех иных объектов плагина.

Добавляем в главный класс PluginName/Plugin способ, для получения DIC :

<?php// ...class Plugin {// ...public function get_service( string $container_name ): ?object {return $this->container_builder->get( $container_name );}// ...}

В результате мы можем отключить абсолютно любой экшн/фильтр нашего плагина. К примеру в классе PluginName\Front\Front у нас подключение стилей:

<?phpnamespace PluginName\Front;class Front {public function hooks(): void {add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_styles' ] );}public function enqueue_styles() {//...}}

При помощи хука plugin_name_init мы можем приобрести главный объект плагина и приобрести объект PluginName\Front\Front используя DIC и использовать отключение хуков:

function remove_plugin_name_actions( $instance ) {    $front = $instance->get_service( 'front' );    if ( ! $front ) {        return;    }    remove_action( 'wp_enqueue_scripts', [ $front, 'enqueue_styles' ] );}add_action( 'plugin_name_init', 'remove_plugin_name_actions' );

Здорово правда?

Использовать DIC это конечно круто и здорово, но не необходимо забывать о том, что иные плагины/темы могут так же использовать его и одним прекрасным днем, вы наткнетесь на конфликты версий плагинов. Так что для всех сторонних пакетов необходимо добавить префиксы.

Префиксы для зависимостей

Mozart , который предложил Mark Jaquith в целом удобный, но справится с префиксами для данных пакетов он не смог, надеюсь когда-нибудь он данному научится. Так что используем
php-scoper . Для начала надо его установить:

composer require bamarni/composer-bin-plugin --devcomposer bin php-scoper config minimum-stability devcomposer bin php-scoper config prefer-stable truecomposer bin php-scoper require --dev humbug/php-scoper

Создаем файл конфигурации scoper.inc.php :

<?phpuse Isolated\Symfony\Component\Finder\Finder;return ['prefix'                     => 'PluginName\\Vendor','whitelist-global-constants' => false,'whitelist-global-classes'   => false,'whitelist-global-functions' => false,'finders'                    => [Finder::create()->files()->in(['vendor/psr/container/','vendor/symfony/config/','vendor/symfony/filesystem/','vendor/symfony/service-contracts/','vendor/symfony/dependency-injection/',])->name( [ '*.php' ] ),],'patchers'                   => [function ( string $file_path, string $prefix, string $contents ): string {return str_replace('Symfony\\\\',sprintf( '%s\\\\Symfony\\\\', addslashes( $prefix ) ),$contents);},],];
  • В prefix указываем префикс для пакетов
  • в finders указываем все пакеты для которых надо добавить префиксы
  • в patchers пишем как мы должны добавить префиксы

При запуске команды, для всех пакетов будет добавлены префиксы:

php-scoper add-prefix --output-dir dependencies/vendor/

Не забываем добавить директиву dependencies/vendor/ для
автозагрузки в composer.json :

// ...  "autoload": {    // ...    "classmap": [      "dependencies/vendor/"    ]  },// ...

Для быстрого запуска php-scoper’а добавим скрипты в composer.json и запустим их на событие установки/обновления composer’а:

// ...  "scripts": {    "install-scoper": [      "composer bin php-scoper config minimum-stability dev",      "composer bin php-scoper config prefer-stable true",      "composer bin php-scoper require --dev humbug/php-scoper"    ],    "scoper": "php-scoper add-prefix --config .scoper.inc.php --output-dir dependencies/vendor/",    "post-install-cmd": [      "composer install-scoper",      "composer scoper",      "composer dump-autoload"    ],    "post-update-cmd": [      "composer install-scoper",      "composer scoper",      "composer dump-autoload"    ]  }// ...

После этого не забудьте заменить оригинальные пакеты на пакеты с префиксами .

Результат

  • В плагине нет Hard Dependencies
  • Плагин загружается не сразу же, а на хук plugins_loaded , что упрощает работу с иными плагинами.
  • Отключить плагин можно при помощи одного вызова remove_action
  • Отключить любой экшн/фильтр плагина можно на хук plugin_name_init
  • Пакеты composer’а не конфликтуют с иными плагина/темами

Как вам такой запуск плагина?

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *