Мы будем идти от плохого архитектурного решения к адекватному.
Основные принципы которые надо соблюдать
- Инкапсуляция – свойства закрыты, получаем их через методы.
- Позднее статическое связывание – делаем абстрактный класс, а потом используем его как шаблон для заполнения потомком.
- Интерфейс чтобы заставить некоторые классы иметь одинаковые методы чтобы потом вызывать в цикле.
Первоначальный вариант – все происходит в одном файле
if (! defined( 'ABSPATH') ) {
exit;
}
class Sprm_Wp_Blocks {
public string $plugin_directory;
private string $acf_json_path = 'config/acf-json';
function __construct () {
$this->plugin_directory = plugin_dir_path(__FILE__);
}
public function init(): void {
add_action('plugins_loaded', [$this, 'setAcfJsonPath']);
add_action('acf/init', [$this, 'registerAcfBlockTypes']);
}
public function setAcfJsonPath(): void {
...
}
/**
* Registers all custom ACF block types for the Gutenberg editor.
*
* Each block has its own render callback, which means styles are only
* enqueued when the block actually appears on the page — not globally.
* WordPress calls the render_callback only for blocks found in the post content,
* so unused blocks never load their assets.
*
* As result, we get enqueued stylesheet only for this block:
* <link rel='stylesheet' id='sprm-block-statistics-css' href='http://pt-bunny.test/wp-content/plugins/sprm-blocks/templates/blocks/statistics-block/style.css?ver=1771520831' media='all' />
*
* @return void
*/
function registerAcfBlockTypes(): void {
acf_register_block_type([
'name' => 'cta-block',
'title' => 'CTA Block',
'description' => 'A block for CTA',
'render_callback' => array($this, 'renderCtaBlock'),
'icon' => 'list-view',
'keywords' => 'cta'
]);
acf_register_block_type([
'name' => 'statistics-block',
'title' => 'Statistics Block',
'description' => 'A block for displaying statistics block',
'render_callback' => array($this, 'renderStatisticsBlock'),
'icon' => 'list-view',
'keywords' => 'statistics'
]);
}
public function renderStatisticsBlock($block, $content = '', $is_preview = false, $post_id = 0): void
{
$handle = 'sprm-block-statistics';
$cssFile = $this->styleFile('statistics-block');
$cssUrl = $this->styleUrl('statistics-block');
// Enqueue the block's stylesheet only once, even if the block appears multiple times.
if (file_exists($cssFile) && !wp_style_is($handle, 'enqueued')) {
wp_enqueue_style($handle, $cssUrl, [], filemtime($cssFile));
}
$template = $this->templatePath('statistics-block');
if (file_exists($template)) {
include $template;
}
}
public function renderCtaBlock($block, $content = '', $is_preview = false, $post_id = 0): void
{
$handle = 'sprm-block-cta';
$cssFile = $this->styleFile('cta-block');
$cssUrl = $this->styleUrl('cta-block');
// Enqueue the block's stylesheet only once, even if the block appears multiple times.
if (file_exists($cssFile) && !wp_style_is($handle, 'enqueued')) {
wp_enqueue_style($handle, $cssUrl, [], filemtime($cssFile));
}
$template = $this->templatePath('cta-block');
if (file_exists($template)) {
include $template;
}
}
}
$instance = new Sprm_Wp_Blocks();
$instance->init();
Проблемы
- Мы видим дублирование кода. Для каждого блока выполняется одинаковая логика:
function acf_register_block_type();
function renderStatisticsBlock() / function renderCtaBlock()
Решение
- Сделать абстрактный класс Block.
- От него будет наследоваться каждый блок чтобы не дублировать код.
- Благодаря статическому связываннию (static::) в методы абстрактного класса будут подставляться значения с наследников.
<?php
namespace Plt\SprmBlocks;
abstract class Block
{
public static string $blockName;
public static string $blockSlug;
public static string $blockTitle;
// init for each block
public static function registerHooks(): void
{
// static::class - returns full name of child Class ("Plt\SprmBlocks\blocks\Statistics")
add_action('acf/init', [static::class, 'registerAcfBlockTypes']);
}
public static function registerAcfBlockTypes(): void {
acf_register_block_type([
'name' => static::$blockName,
'title' => static::$blockTitle,
'render_callback' => [static::class, 'renderBlock'],
'icon' => 'list-view',
'keywords' => static::$blockTitle
]);
}
abstract static function renderBlock($block, $content, $is_preview, $post_id): void;
}
И теперь наследуемся от абстрактного блока:
// Statistics block
<?php
namespace Plt\SprmBlocks\blocks;
use Plt\SprmBlocks\Block;
use Sprm_Wp_Blocks;
class Statistics extends Block
{
public static string $blockName = 'statistics-block';
public static string $blockSlug = 'sprm-block-statistics';
public static string $blockTitle = 'Statistics Block';
public static function renderBlock($block, $content = '', $is_preview = false, $post_id = 0): void
{
$cssFile = Sprm_Wp_Blocks::styleFile(self::$blockName);
$cssUrl = Sprm_Wp_Blocks::styleUrl(self::$blockName);
// Enqueue the block's stylesheet only once, even if the block appears multiple times.
if (file_exists($cssFile) && !wp_style_is(self::$blockSlug, 'enqueued')) {
wp_enqueue_style(self::$blockSlug, $cssUrl, [], filemtime($cssFile));
}
$template = Sprm_Wp_Blocks::templatePath(self::$blockName);
if (file_exists($template)) {
include $template;
}
}
}
// CTA Block
<?php
namespace Plt\SprmBlocks\blocks;
use Plt\SprmBlocks\Block;
use Sprm_Wp_Blocks;
class Cta extends Block
{
public static string $blockName = 'cta-block';
public static string $blockSlug = 'sprm-block-cta';
public static string $blockTitle = 'CTA Block';
public static function renderBlock($block, $content = '', $is_preview = false, $post_id = 0): void
{
$cssFile = Sprm_Wp_Blocks::styleFile(self::$blockName);
$cssUrl = Sprm_Wp_Blocks::styleUrl(self::$blockName);
// Enqueue the block's stylesheet only once, even if the block appears multiple times.
if (file_exists($cssFile) && !wp_style_is(self::$blockSlug, 'enqueued')) {
wp_enqueue_style(self::$blockSlug, $cssUrl, [], filemtime($cssFile));
}
$template = Sprm_Wp_Blocks::templatePath(self::$blockName);
if (file_exists($template)) {
include $template;
}
}
}
И сделал абстрактный класс Plugin:
<?php
namespace Plt\SprmBlocks;
abstract class Plugin
{
public static string $plugin_directory;
public static string $plugin_url;
public static string $acf_json_path;
public static function templatePath(string $block): string
{
return static::$plugin_directory . 'templates/blocks/' . $block . '/' . $block . '.php';
}
public static function styleFile(string $block): string
{
return trailingslashit(static::$plugin_directory) . "templates/blocks/{$block}/style.css";
}
public static function styleUrl(string $block): string
{
return static::$plugin_url . "templates/blocks/{$block}/style.css";
}
}
Наследуемся:
<?php
use Plt\SprmBlocks\blocks\Cta;
use Plt\SprmBlocks\blocks\Statistics;
use Plt\SprmBlocks\Plugin;
if (! defined( 'ABSPATH') ) {
exit;
}
require_once __DIR__ . '/vendor/autoload.php';
class Sprm_Wp_Blocks extends Plugin {
public static string $plugin_directory;
public static string $plugin_url;
public static string $acf_json_path = 'config/acf-json';
function __construct () {
self::$plugin_directory = plugin_dir_path(__FILE__);
self::$plugin_url = plugin_dir_url(__FILE__);
}
public function init(): void {
add_action('plugins_loaded', [$this, 'setAcfJsonPath']);
// Blocks
Statistics::registerHooks();
Cta::registerHooks();
}
public function setAcfJsonPath(): void {
add_filter('acf/settings/load_json', function(array $paths) {
// Remove the default ACF load path so only our path is used.
unset($paths[0]);
$paths[] = trailingslashit(self::$plugin_directory) . self::$acf_json_path;
return $paths;
});
}
}
$instance = new Sprm_Wp_Blocks();
$instance->init();
Уже намного лучше, блоки не дублируют код друг друга.
Инкапсуляция
Смысл в том, что свойства обычно делают private или protected, а взаимодействие с ними происходит только через public методы для доступа.
Геттер – возвращает значение свойства.
Сеттер – устанавливает или изменяет его.
Сейчас у нас свойства public и мы обращаемся к ним напрямую.
abstract class Block
{
public static string $blockName;
public static string $blockTitle;
public static function registerAcfBlockTypes(): void {
acf_register_block_type([
'name' => static::$blockName,
'title' => static::$blockTitle,
....
Переделал.
<?php
namespace Plt\SprmBlocks;
abstract class Plugin
{
private static array $instances = [];
protected string $plugin_file;
protected string $acf_json_path = '';
final private function __construct()
{
}
final public static function getInstance(): static
{
$class = static::class;
if (! isset(self::$instances[$class])) {
self::$instances[$class] = new static();
}
return self::$instances[$class];
}
abstract public function init(): void;
public static function templatePath(string $block): string
{
return static::pluginDir() . "templates/blocks/{$block}/{$block}.php";
}
public static function styleFile(string $block): string
{
return static::pluginDir() . "templates/blocks/{$block}/style.css";
}
public static function styleUrl(string $block): string
{
return static::pluginUrl() . "templates/blocks/{$block}/style.css";
}
public static function pluginDir(): string
{
return plugin_dir_path(static::getInstance()->plugin_file);
}
public static function pluginUrl(): string
{
return plugin_dir_url(static::getInstance()->plugin_file);
}
public static function acfJsonPath(): string
{
return static::pluginDir() . static::getInstance()->acf_json_path;
}
}
Проблема
- Если на одной странице 2 одинаковых блока. То addActionOnce / addFilterOnce — умное решение реальной WordPress-проблемы: блок может рендериться несколько раз, и хуки не должны дублироваться.
- DTO
- Dependency Injection?
- contracts
- Сделать класс для путей? получении папки плагина и тд..
- подклчать классы hookable в цикле
- Как обрабатывать и выбрасывать ошибки
- Сделать Мастер плагин для наших плагинов или хотя бы засовывать их в одну группу в меню
- лоигку в классах можно разбить еще на классы?
Singleton Trait
Решил вынести реализацию Singleton в отдельный Trait, чтобы не писать эту логику в классе плагина.
До этого писали в абстрактном классе Plugin:
abstract class Plugin
{
private static array $instances = [];
protected string $plugin_file;
protected string $acf_json_path = '';
final private function __construct()
{
}
final public static function getInstance(): static
{
$class = static::class;
if (! isset(self::$instances[$class])) {
self::$instances[$class] = new static();
}
return self::$instances[$class];
}
...
Сделал отдельный Trait:
<?php
namespace Plt\SprmBlocks\Traits;
trait Singleton
{
private static $instance = null;
// Private constructor to prevent direct instantiation
private function __construct()
{
}
// Prevent cloning the instance
private function __clone()
{
}
// Prevent serialization
private function __sleep()
{
}
// Prevent deserialization
private function __wakeup()
{
}
/**
* Get the single instance of the class.
*
* @return static
*/
public static function getInstance()
{
if (static::$instance === null) {
static::$instance = new static();
}
return static::$instance;
}
}
И теперь используем:
class SprmWpBlocks extends Plugin
{
use Singleton;
...
}
SprmWpBlocks::getInstance()->init();
Нет смысла в классе abstract Plugin
- Один наследник.
SprmWpBlocks— единственный класс, который его расширяет. Абстрактный класс нужен когда есть общая логика для нескольких реализаций. У тебя одна. - Не переиспользуется. Это не библиотека которую ты подключаешь в 5 плагинов. Это конкретный плагин.
- Добавляет indirection без пользы. Чтобы понять что делает
SprmWpBlocks, нужно смотреть два файла вместо одного.
Поэтому его удалил, сделал просто Interface и все повыносил в классы:
// Interface
interface Pluginable
{
public function init(): void;
}
// Plugin
class SprmWpBlocks implements Pluginable
{
use Singleton;
protected string $plugin_file = __FILE__;
public function init(): void
{
Assets::registerHooks();
I18n::registerHooks();
Acf::registerHooks();
// Blocks
Statistics::registerHooks();
Cta::registerHooks();
AffiliateModels::registerHooks();
}
}
SprmWpBlocks::getInstance()->init();