ساختار استاندارد افزونه وردپرس، خط قرمزی است که یک اسکریپت آماتور را از یک محصول قابل نگهداری جدا می‌کند. من در طول سال‌ها کار روی افزونه‌های اختصاصی و بازبینی صدها پلاگین متن‌باز، الگویی تکراری دیده‌ام: پروژه‌هایی که در ماه ششم به دیوار می‌خورند، همیشه ریشه در ساختار پوشه‌ای دارند که با رشد کد مقیاس نگرفته. در مقابل، افزونه‌هایی که سال‌ها بدون بازنویسی دوام می‌آورند، از همان کامیت اول طوری چیده شده‌اند که افزودن ویژگی جدید هزینه‌ای به تیم تحمیل نکند.

در این راهنما ساختاری را که خودم در پروژه‌های واقعی به آن رسیده‌ام کامل باز می‌کنم. اگر تازه‌کار هستید و می‌خواهید بدانید افزونه در معماری وردپرس چه جایگاهی دارد، پیش از ادامه افزونه وردپرس چیست و چگونه افزونه مناسب انتخاب کنیم را بخوانید. این مقاله، لایه بعدی همان بحث است: از فایل اصلی و هدرش شروع می‌کنم و تا الگوی autoloading با Composer و ساختار قابل تست پیش می‌روم.

چرا ساختار افزونه سرنوشت پروژه را تعیین می‌کند

در جلسه‌های مشاوره زیاد شنیده‌ام «افزونه که کار می‌کند، ساختارش مهم نیست». این جمله دقیقاً همان تفکری است که پروژه را در ماه ششم به بن‌بست می‌رساند. ساختار پوشه و نام‌گذاری، انتخاب زیبایی‌شناختی نیست؛ سه هزینه پنهان را مستقیماً کنترل می‌کند: هزینه افزودن ویژگی، هزینه رفع باگ اضطراری، و هزینه انتقال پروژه به توسعه‌دهنده دیگر.

تجربه میدانی من این است که تفاوت ساختار خوب و بد را می‌شود در سه موقعیت خاص دقیق اندازه گرفت. اول، وقتی مشتری صبح زنگ می‌زند که سایت خوابیده و باید در سی دقیقه علت را پیدا کنید. دوم، وقتی قرار است قابلیت جدیدی به افزونه اضافه شود و باید تخمین بزنید یک روز طول می‌کشد یا یک هفته. سوم، وقتی تیم عوض می‌شود و کسی که کد را نوشته دیگر در دسترس نیست. ساختار حرفه‌ای در هر سه موقعیت، هزینه را نصف می‌کند.

پایه این بحث همان اصولی است که در استانداردهای کدنویسی وردپرس چیست توضیح داده شده؛ اما آن نوشته روی قواعد نگارش تمرکز دارد و این یکی روی چیدمان پوشه، مرزهای کلاس و جداسازی مسئولیت‌ها. این دو مکمل یکدیگرند و بدون هم ابتر می‌مانند.

ساختار پوشه، هزینه آینده است؛ هرچه امروز ساده‌تر و شفاف‌تر بچینید، فردا ارزان‌تر تغییرش می‌دهید.

آناتومی یک افزونه: از هدر تا آخرین خط

هر افزونه وردپرس، هرچقدر هم پیچیده باشد، از یک نقطه شروع می‌شود: یک فایل PHP در ریشه پوشه افزونه که هدر استاندارد دارد. وردپرس این فایل را می‌خواند، هدر را تجزیه می‌کند و افزونه را در فهرست پیشخوان نمایش می‌دهد. همه چیز از همین فایل آغاز می‌شود.

فایل اصلی و هدر افزونه

هدر افزونه مجموعه‌ای از کلید-مقدارها است که وردپرس برای شناسایی استفاده می‌کند. حداقل کلیدهای ضروری عبارتند از Plugin Name، Description، Version، Author، License و Text Domain. کلید آخر بدون اغراق مهم‌ترین قسمت هدر است، چون به i18n گره خورده و اگر نباشد، افزونه در مخزن رسمی وردپرس پذیرفته نمی‌شود.

<?php
/**
 * Plugin Name:       My Professional Plugin
 * Plugin URI:        https://example.com/my-professional-plugin
 * Description:       A production-grade WordPress plugin.
 * Version:           1.0.0
 * Requires at least: 6.0
 * Requires PHP:      7.4
 * Author:            Your Name
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       my-professional-plugin
 * Domain Path:       /languages
 */

دو کلید Requires at least و Requires PHP در سال‌های اخیر اهمیت بیشتری یافته‌اند، چون به وردپرس می‌گویند این افزونه در کدام محیط امن است. اگر افزونه‌ای این دو کلید را نداشته باشد، کاربر می‌تواند روی PHP نسخه قدیمی نصبش کند و سایت را با fatal error از کار بیندازد. روی جزئیات رفتاری این هدر در ساختار فایل‌های یک افزونه استاندارد وردپرس دقیق‌تر بحث کرده‌ام.

پوشه‌های استاندارد

یک افزونه حرفه‌ای معمولاً از این پوشه‌ها استفاده می‌کند:

  • includes/ یا src/ برای کلاس‌ها و منطق اصلی
  • admin/ برای کدهای مخصوص پیشخوان
  • public/ یا frontend/ برای کدهای سمت نمایش
  • assets/ برای CSS، JS و تصاویر
  • languages/ برای فایل‌های .pot و .po
  • templates/ برای قالب‌های قابل بازنویسی توسط قالب سایت
  • tests/ برای تست‌های PHPUnit
  • vendor/ برای وابستگی‌های Composer

تقسیم پیشخوان و فرانت، یک اصل معماری است نه سلیقه. کد پیشخوان نباید در هر درخواست کاربر نهایی لود شود و کد فرانت نباید در هر بازدید پیشخوان بار شود. رعایت نکردن این جداسازی، مصرف حافظه و زمان بوت افزونه را محسوس بالا می‌برد. یک قاعده تجربی که در پروژه‌ها رعایت می‌کنم: هر پوشه‌ای که در فهرست بالا آمده و در پروژه شما خالی می‌ماند، یا باید حذف شود یا نشانه‌ای است که هنوز بخشی از معماری را پیاده نکرده‌اید.

فایل‌های همراه ضروری

علاوه بر پوشه‌ها، این فایل‌ها تقریباً همیشه در یک افزونه حرفه‌ای حضور دارند:

  • readme.txt با فرمت استاندارد مخزن وردپرس
  • composer.json و در صورت نیاز composer.lock
  • uninstall.php برای پاک‌سازی کامل هنگام حذف
  • .gitignore برای کنار گذاشتن vendor/ و node_modules/
  • phpcs.xml.dist برای پیکربندی استاندارد کد
  • phpunit.xml.dist برای پیکربندی تست

فایل uninstall.php جایگاه ویژه‌ای دارد. اگر گزینه‌ها و جدول‌های اختصاصی افزونه را هنگام حذف پاک نکنید، بعد از چند نصب و حذف، دیتابیس کاربر پر از ردیف‌های یتیم می‌شود. این بدترین نوع بدهی فنی است چون کاربر نهایی نمی‌فهمد چه چیزی دیتابیسش را سنگین کرده. یک الگوی استاندارد برای این فایل دارم که در همه پروژه‌ها تکرار می‌کنم: تنها زمانی پاک‌سازی کامل انجام بده، که کاربر در تنظیمات صریحاً خواسته باشد؛ وگرنه فقط transientها و cronها را پاک کن و بقیه را نگه دار.

هدر افزونه شناسنامه است؛ readme.txt دفترچه راهنما؛ uninstall.php پاک‌کننده آخر. نبود هیچ‌کدام، امضای یک پلاگین آماتور است.

الگوی ساختار پوشه: از ساده تا حرفه‌ای

در پروژه‌های واقعی، سه الگوی ساختار پوشه دیده‌ام که هر کدام جای خودش را دارد. انتخاب الگو باید بر اساس پیچیدگی پروژه انجام شود، نه بر اساس مُد روز.

الگوی مینیمال: برای پلاگین‌های تک‌منظوره

اگر افزونه‌ای کمتر از پانصد خط کد و یک قابلیت مشخص دارد، ساختار پیچیده به آن آسیب می‌زند. الگوی مینیمال چیزی شبیه این است:

my-plugin/
├── my-plugin.php
├── uninstall.php
├── readme.txt
└── assets/
    └── css/
        └── admin.css

در این الگو همه منطق در فایل اصلی است. محدودیت واقعی این الگو، جایی است که تعداد توابع از بیست رد شود؛ آن‌جا باید به الگوی متوسط مهاجرت کنید.

الگوی متوسط: برای پروژه‌های چندمنظوره

وقتی افزونه چند ماژول مستقل دارد، پوشه‌بندی باید همان ماژول‌ها را بازتاب دهد:

my-plugin/
├── my-plugin.php
├── includes/
│   ├── class-plugin.php
│   ├── class-activator.php
│   ├── class-deactivator.php
│   ├── class-i18n.php
│   └── class-loader.php
├── admin/
│   ├── class-admin.php
│   ├── partials/
│   └── css/
├── public/
│   ├── class-public.php
│   ├── partials/
│   └── css/
├── languages/
└── uninstall.php

این الگو، همان ساختاری است که در بسیاری از افزونه‌های متن‌باز موفق دیده می‌شود: یک کلاس اصلی که بقیه را بار می‌کند، یک کلاس برای هر دامنه مسئولیت، و پوشه‌های جدا برای پیشخوان و فرانت. نقطه قوت این الگو، خوانایی آن برای تیم است: هر توسعه‌دهنده جدید در ده دقیقه می‌فهمد کجای کد چه کاری انجام می‌دهد.

الگوی حرفه‌ای: با Composer و namespace

برای افزونه‌ای که می‌خواهد سال‌ها قابل نگهداری بماند و در چند سایت یا چند مشتری مستقر شود، الگوی حرفه‌ای جواب می‌دهد:

my-plugin/
├── my-plugin.php
├── composer.json
├── composer.lock
├── autoload.php
├── src/
│   ├── Plugin.php
│   ├── Activator.php
│   ├── Deactivator.php
│   ├── Admin/
│   │   ├── AdminService.php
│   │   ├── SettingsPage.php
│   │   └── Notices.php
│   ├── Frontend/
│   │   ├── FrontendService.php
│   │   └── Shortcodes.php
│   ├── Rest/
│   │   └── RestController.php
│   ├── Support/
│   │   ├── Container.php
│   │   └── Helpers.php
│   └── Contracts/
│       └── ServiceInterface.php
├── templates/
│   └── admin/
│       └── settings-page.php
├── assets/
│   ├── css/
│   ├── js/
│   └── images/
├── languages/
├── tests/
│   ├── Unit/
│   └── Integration/
├── vendor/
├── uninstall.php
└── readme.txt

تفاوت این الگو با قبلی، نه در تعداد پوشه‌ها است بلکه در تفکیک معنایی: هر پوشه یک دامنه مسئولیت است، نه یک لایه فنی. مثلاً پوشه Admin/ هم سرویس، هم صفحه تنظیمات و هم notice‌ها را در خود دارد، چون همه‌شان زیر دامنه پیشخوان قرار می‌گیرند. این رویکرد که به آن «بسته‌بندی بر اساس دامنه» می‌گویند، مقیاس‌پذیری بهتری از بسته‌بندی بر اساس نوع فایل دارد. برای پروژه‌ای که تیم روی آن کار می‌کند، انتخاب بین این دو رویکرد یک تصمیم معماری است نه انتخاب سبک. تفاوت را وقتی در جلسه‌های تیم من دیدم، بعد از افزودن پانزدهمین قابلیت روشن شد: در بسته‌بندی بر اساس دامنه، هر قابلیت جدید در یک پوشه مستقل جا می‌گیرد؛ در بسته‌بندی فنی، فایل‌ها در همه پوشه‌ها پخش می‌شوند.

Namespace و Composer Autoload

وقتی افزونه از الگوی متوسط رد می‌شود، بزرگ‌ترین ریسک این است که نام کلاس‌هایش با افزونه دیگری تداخل کند. اینجاست که namespace وارد می‌شود.

چرا namespace بدون اغراق ضروری است

در PHP مدرن، دو افزونه که هر دو کلاسی به نام Settings تعریف کنند، بسته به ترتیب لود، یکی از آن‌ها fatal error می‌دهد. با namespace، این کلاس‌ها به دو فضای نام متفاوت منتقل می‌شوند و هیچ تداخلی پیش نمی‌آید. قاعده ساده‌ای که برای خودم گذاشته‌ام: هر کلاسی که در پوشه src/ قرار می‌گیرد، باید namespace داشته باشد؛ کلاس بدون namespace در src/، نشانه بی‌توجهی به این اصل است.

پیکربندی PSR-4 در Composer

با Composer، نگاشت namespace به پوشه بسیار ساده است:

{
  "name": "yourname/my-professional-plugin",
  "description": "A production-grade WordPress plugin.",
  "type": "wordpress-plugin",
  "license": "GPL-2.0-or-later",
  "require": {
    "php": ">=7.4"
  },
  "require-dev": {
    "phpunit/phpunit": "^9.5",
    "wp-coding-standards/wpcs": "^3.0"
  },
  "autoload": {
    "psr-4": {
      "MyVendor\\MyPlugin\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "MyVendor\\MyPlugin\\Tests\\": "tests/"
    }
  }
}

با این پیکربندی، کلاسی که در مسیر src/Admin/SettingsPage.php قرار دارد، به‌طور خودکار با نام کامل MyVendor\MyPlugin\Admin\SettingsPage شناخته می‌شود. در فایل اصلی افزونه هم فقط یک خط برای لود autoloader کافی است:

if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) {
    require_once __DIR__ . '/vendor/autoload.php';
}

یک هشدار عملی: اگر افزونه‌تان را از طریق مخزن رسمی وردپرس توزیع می‌کنید، معمولاً پوشه vendor/ را همراه نمی‌فرستند و کاربر باید آن را خودش با composer install بسازد؛ برای پروژه‌های خصوصی این محدودیت وجود ندارد و می‌توانید پوشه را کامل همراه ببرید. برای درک نحوه تعامل این ساختار با هوک‌ها و لودر اصلی افزونه، افزونه وردپرس چطور نوشته می‌شود؟ را به‌عنوان مکمل این بخش پیشنهاد می‌کنم.

namespace هزینه‌ای ندارد؛ نبودش هزینه دارد. هر کلاسی که در فضای نام جهانی رها شود، یک بمب ساعتی برای سایت مشتری است.

جداسازی منطق با کلاس‌های اختصاصی

مهم‌ترین اصل در ساختار یک افزونه حرفه‌ای این است که هر کلاس یک مسئولیت داشته باشد. این اصل که در ادبیات مهندسی نرم‌افزار به Single Responsibility Principle (SRP) شناخته می‌شود، در وردپرس بیشتر از هر جای دیگری رعایت نمی‌شود و همین عدم رعایت، دلیل اصلی افزونه‌های غیرقابل نگهداری است.

الگوی کلاس اصلی و لودر

کلاس اصلی افزونه باید فقط کار هماهنگ‌کننده انجام دهد: خواندن autoloader، ساخت سرویس‌ها، و ثبت هوک‌ها در لودر. خودش نباید هیچ منطق تجاری داشته باشد:

namespace MyVendor\MyPlugin;

final class Plugin {

    private static ?Plugin $instance = null;

    public static function instance(): Plugin {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {}

    public function boot(): void {
        add_action( 'init', [ $this, 'registerServices' ] );
        add_action( 'admin_menu', [ $this, 'registerAdminMenu' ] );
    }
}

این الگو که در انگلیسی Singleton نامیده می‌شود، برای نقطه ورود افزونه مناسب است، ولی باید در جای درست استفاده شود. اگر همه کلاس‌های افزونه Singleton شوند، تست‌پذیری از بین می‌رود و وابستگی‌ها پنهان می‌شوند. برای سرویس‌ها، بهتر است وابستگی‌ها را در سازنده تزریق کنید.

قاعده وابستگی‌ها

یک قاعده تجربی که در تیم‌های حرفه‌ای رعایت می‌کنم: هیچ‌وقت یک سرویس را مستقیم از داخل سرویس دیگر نمونه‌سازی نکنید. اگر AdminService به SettingsPage نیاز دارد، این نیاز باید از سازنده‌اش عبور کند:

namespace MyVendor\MyPlugin\Admin;

final class AdminService {

    public function __construct(
        private SettingsPage $settings_page
    ) {}

    public function register(): void {
        add_action( 'admin_menu', [ $this->settings_page, 'register' ] );
    }
}

مزیت این الگو دو چیز است: اول، در تست می‌توانید نسخه fake سرویس‌ها را جایگزین کنید؛ دوم، وابستگی‌های افزونه روی یک نگاه در سازنده‌ها قابل خواندن است. کد وابستگی‌پنهان، کدی است که شش ماه بعد کسی جرات تغییرش را ندارد.

ثبت هوک‌ها هم باید در همین چارچوب باشد. اگر با هوک‌ها آشنا نیستید، پیش از پیاده‌سازی، نحوه استفاده صحیح از هوک‌های وردپرس را بخوانید؛ در ساختار حرفه‌ای، هر سرویس خودش مسئول ثبت هوک‌های مربوط به دامنه خودش است. تفاوت Action و Filter را هم اگر باید دقیق بدانید، تفاوت اکشن و فیلتر در وردپرس مرجع کوتاهی است.

مدیریت Assets به روش استاندارد

یکی از رایج‌ترین خطاهای ساختاری، بارگذاری نامنظم CSS و JS است. یک افزونه حرفه‌ای سه قاعده را در مدیریت assets رعایت می‌کند: انصراف از بارگذاری در صفحه‌های نامرتبط، نسخه‌بندی صحیح برای کش، و جداسازی شرطی پیشخوان و فرانت.

public function enqueueAdminAssets( string $hook ): void {
    if ( 'toplevel_page_my-plugin' !== $hook ) {
        return;
    }

    wp_enqueue_style(
        'my-plugin-admin',
        plugin_dir_url( __FILE__ ) . 'assets/css/admin.css',
        [],
        MY_PLUGIN_VERSION
    );

    wp_enqueue_script(
        'my-plugin-admin',
        plugin_dir_url( __FILE__ ) . 'assets/js/admin.js',
        [ 'wp-api-fetch' ],
        MY_PLUGIN_VERSION,
        true
    );
}

شرط اول این تابع، نیمی از بودجه کارایی افزونه را نجات می‌دهد. بدون آن، فایل CSS و JS در تمام صفحات پیشخوان بارگذاری می‌شود و روی سایت‌های چندافزونه‌ای، هر صفحه پیشخوان کندتر باز می‌شود. برای شروع ساخت یک صفحه تنظیمات که این assetها را مصرف کند، ساخت صفحه تنظیمات اختصاصی در وردپرس مسیر مرحله‌به‌مرحله‌ای دارد.

نکته دوم، نسخه‌بندی است. ثابت MY_PLUGIN_VERSION که در فایل اصلی تعریف می‌شود، باید در هر انتشار به‌روز شود. اگر این کار را فراموش کنید، مرورگر کاربر نسخه قدیمی فایل را سرو می‌کند و از نظر کاربر، به‌روزرسانی اعمال نشده است — درحالی‌که از نظر شما کد جدید روی سایت است. این باگ در پروژه‌های واقعی بیشتر از آن‌که فکر می‌کنید رخ می‌دهد.

بین‌المللی‌سازی و فایل‌های زبان

حتی اگر افزونه‌ای قرار نیست ترجمه شود، ساختار i18n باید از روز اول رعایت شود؛ افزودنش بعد از انتشار، بازنویسی همه رشته‌های متنی است. قاعده ساده است: هر رشته متنی که کاربر می‌بیند، باید از توابع ترجمه عبور کند و به text domain افزونه گره بخورد.

__( 'Settings saved.', 'my-professional-plugin' );
esc_html__( 'Save changes', 'my-professional-plugin' );
printf(
    /* translators: %s: user name */
    esc_html__( 'Hello, %s', 'my-professional-plugin' ),
    esc_html( $user_name )
);

text domain در هدر افزونه و در همه فراخوانی‌های ترجمه باید یکی باشد. در فایل اصلی، عملگر بارگذاری فایل‌های ترجمه را در هوک init ثبت کنید:

add_action( 'init', function () {
    load_plugin_textdomain(
        'my-professional-plugin',
        false,
        dirname( plugin_basename( __FILE__ ) ) . '/languages'
    );
} );

برای تولید فایل .pot پایه، از ابزار WP-CLI استفاده می‌کنم؛ دستور ساده آن است: wp i18n make-pot . languages/my-professional-plugin.pot. اگر text domain یا مسیر فایل‌ها را اشتباه تنظیم کرده باشید، این دستور رشته‌ای پیدا نمی‌کند و این خودش یک تست غیرمستقیم برای صحت ساختار i18n است.

امنیت ساختاری در افزونه

امنیت افزونه از ساختار شروع می‌شود، نه از افزودن قابلیت‌های امنیتی در پایان. سه ستون امنیت ساختاری در هر افزونه حرفه‌ای وجود دارد: جلوگیری از دسترسی مستقیم به فایل‌های PHP، اعتبارسنجی و پاک‌سازی ورودی‌ها، و استفاده درست از nonce در فرم‌ها.

جلوگیری از دسترسی مستقیم

ابتدای هر فایل PHP که مستقیم در مرورگر قابل فراخوانی است، این خط را اضافه کنید:

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

این یک الگوی استاندارد است و نبودش در فایلی که از بیرون قابل فراخوانی است، در بازبینی افزونه‌های حرفه‌ای بلافاصله علامت قرمز محسوب می‌شود.

اعتبارسنجی و پاک‌سازی

هر داده‌ای که از کاربر می‌آید باید اعتبارسنجی و پاک‌سازی شود. این دو عمل را در جای درست انجام دهید: پاک‌سازی هنگام ذخیره در دیتابیس، escape کردن هنگام نمایش. جزئیات دقیق این تفکیک را در نوشتن کد PHP امن برای وردپرس باز کرده‌ام؛ خلاصه‌اش این است که توابع sanitize_text_field، absint، wp_kses_post و مشابه، جای خودشان را دارند و نمی‌شود همه را به یک تابع عمومی تقلیل داد. لایه مکمل این بحث، پاک‌سازی داده‌ها در کدنویسی وردپرس است.

Nonce در فرم‌ها و درخواست‌های AJAX

هر فرم یا درخواست AJAX که وضعیت را تغییر می‌دهد باید nonce داشته باشد. این قانون در افزونه‌های حرفه‌ای استثنا ندارد:

// هنگام ساخت فرم
wp_nonce_field( 'my_plugin_save_settings', 'my_plugin_nonce' );

// هنگام پردازش فرم
if ( ! isset( $_POST['my_plugin_nonce'] )
     || ! wp_verify_nonce(
         sanitize_text_field( wp_unslash( $_POST['my_plugin_nonce'] ) ),
         'my_plugin_save_settings'
     )
) {
    wp_die( esc_html__( 'Invalid request.', 'my-professional-plugin' ) );
}

جزئیات دقیق‌تر این الگو و دام‌هایی که معمولاً در پیاده‌سازی رخ می‌دهد، در نانس وردپرس و نقش آن در امنیت فرم‌ها توضیح داده شده. یک اشتباه رایج که در بازبینی‌ها زیاد دیده‌ام: قرار دادن nonce در فیلد مخفی بدون بررسی وجود آن در سمت سرور؛ این کار هیچ امنیتی اضافه نمی‌کند و فقط ظاهر کد را امن می‌کند.

امنیت ساختاری یعنی توابع امنیتی در جای درست؛ امنیت تزئینی یعنی فراخوانی توابع امنیتی بدون درک نقششان. تفاوت این دو، تفاوت یک افزونه حرفه‌ای با یک افزونه آماتور است.

تست‌پذیری و ساختار قابل نگهداری

یک افزونه حرفه‌ای، تست‌پذیر است. اما تست‌پذیری از روز اول در ساختار کد ریشه می‌گیرد و اگر بعداً به آن فکر کنید، باید همه چیز را بازنویسی کنید. سه اصل ساختاری که تست‌پذیری را ممکن می‌کند:

  1. تزریق وابستگی از سازنده به‌جای ساخت نمونه داخل کلاس
  2. جدا کردن منطق از فراخوانی مستقیم توابع وردپرس در کلاس‌های قابل تست، با پوشاندن آن‌ها در متدهای نازک
  3. پرهیز از استفاده مستقیم از global؛ وردپرس پر از متغیرهای global است، ولی کد شما نباید در آن‌ها غرق شود

برای افزونه‌ای که با این اصول نوشته شود، تست‌های PHPUnit می‌توانند بدون راه‌اندازی وردپرس اجرا شوند. این سرعت تست را از چند دقیقه به چند ثانیه کاهش می‌دهد و همین تفاوت، تیم را از اجرای تست در هر کامیت منصرف نمی‌کند. کنار این، یک قاعده دیگر هم در پروژه‌ها رعایت می‌کنم: افزونه باید بدون هیچ قالب خاصی هم بتواند فعال شود و کار کند. اگر برای تست افزونه، به قالب خاصی وابسته‌اید، یعنی مرزهای ساختاری را رعایت نکرده‌اید.

لایه بالاتر تست، تست‌های integration با WordPress Test Suite است که برای بررسی تعامل افزونه با هسته وردپرس لازم می‌شود. این تست‌ها کندترند ولی برای اطمینان از سازگاری با نسخه‌های جدید وردپرس، ارزششان انکارناپذیر است. کنار این‌ها، تست سازگاری با افزونه‌های دیگر هم بخشی از ساختار حرفه‌ای است؛ مخصوصاً وقتی افزونه شما هوک‌هایی را تغییر می‌دهد که ممکن است با افزونه دیگری تداخل کند.

مسئله دیتابیس را هم جدی بگیرید: هر جدول اختصاصی که افزونه می‌سازد، باید هنگام فعال‌سازی با dbDelta ساخته شود و هنگام غیرفعال‌سازی (نه حذف) دست‌نخورده باقی بماند. استفاده از dbDelta مزیت مهمی دارد: ساختار جدول را با نسخه‌های بعدی هماهنگ نگه می‌دارد و اگر ستونی اضافه کردید، در به‌روزرسانی افزونه خودش اعمال می‌شود.

پرسش‌های پرتکرار درباره ساختار افزونه وردپرس

آیا برای افزونه‌های کوچک هم باید از Composer و namespace استفاده کنم؟ نه لزوماً. اگر افزونه زیر پانصد خط کد دارد و قابلیت مشخصی را اضافه می‌کند، ساختار مینیمال با توابع پیشونددار کافی است. اما وقتی تعداد توابع از بیست رد شود یا افزونه قرار است توزیع شود، مهاجرت به Composer منطقی است.

چرا نباید کد را مستقیم در فایل اصلی افزونه نوشت؟ چون فایل اصلی جایی است که وردپرس بارگذاری می‌کند، نه جایی که منطق زندگی می‌کند. نوشتن منطق در فایل اصلی، تست را غیرممکن می‌کند، خوانایی را از بین می‌برد و در بازبینی افزونه، پرچم قرمز است.

تفاوت پوشه includes/ و src/ چیست؟ از نظر کارکردی هیچ تفاوتی ندارند؛ یک انتخاب سلیقه‌ای است. من در پروژه‌های جدید src/ را ترجیح می‌دهم چون قرارداد استاندارد PSR-4 روی آن بنا شده و برای توسعه‌دهنده تازه، معنای واضح‌تری دارد.

آیا باید پوشه vendor/ را در مخزن گیت نگه دارم؟ اگر پروژه خصوصی است و از طریق مخزن رسمی وردپرس توزیع نمی‌شود، نگه‌داشتن آن استقرار را ساده‌تر می‌کند. اگر پروژه قرار است در مخزن رسمی پذیرفته شود، فقط composer.json و composer.lock را نگه دارید و کاربر خودش composer install اجرا کند.

آیا استفاده از Singleton برای کلاس اصلی افزونه اشتباه است؟ برای نقطه ورود افزونه، Singleton الگوی مناسبی است. اما استفاده از آن برای همه سرویس‌ها تست‌پذیری را از بین می‌برد. تفکیک درست این است: Singleton برای کلاس اصلی، تزریق وابستگی برای سرویس‌ها.

اگر افزونه‌ام نیاز به نوع نوشته سفارشی (CPT) دارد، کدش کجا برود؟ در پوشه src/PostTypes/ یا معادل دامنه‌ای آن. جزئیات پیاده‌سازی این الگو را در ساخت نوع نوشته سفارشی در وردپرس باز کرده‌ام. برای حفظ جداسازی مسئولیت، ثبت CPT نباید در کلاس اصلی افزونه انجام شود.

چطور بفهمم ساختار افزونه‌ام در سطح حرفه‌ای است؟ یک آزمون ساده در ذهنم دارم: اگر یک توسعه‌دهنده دیگر بیاید و بخواهد قابلیت جدیدی اضافه کند، باید بدون پرسیدن سوال از شما بتواند این کار را انجام دهد. اگر این شرط برقرار نیست، ساختار جای کار دارد.

ساختار درست، سرمایه‌گذاری بلندمدت

ساختار استاندارد افزونه وردپرس، خرج اضافه نیست؛ بیمه‌نامه است. من در پروژه‌هایی که ساختار را جدی گرفته‌ام، بعد از دو سال بدون هیچ بازنویسی، قابلیت‌های جدید اضافه کرده‌ام؛ در پروژه‌هایی که ساختار را نادیده گرفته‌ام، بعد از شش ماه بازنویسی کامل را انتخاب کرده‌ام، چون اصلاح ساختار بد هزینه‌ای بیشتر از ساخت دوباره داشت. تفاوت این دو مسیر، چیزی نیست جز همان چند ساعت اضافه‌ای که در روز اول صرف چیدن پوشه‌ها و کلاس‌ها کرده‌اید.

اگر همین امروز می‌خواهید قدم عملی بردارید، این ترتیب را پیشنهاد می‌کنم: اول autoloader را با Composer راه‌اندازی کنید، دوم کلاس اصلی افزونه را از فایل ریشه جدا کنید، سوم پوشه‌های پیشخوان و فرانت را از هم تفکیک کنید، و چهارم یک تست ساده PHPUnit برای یکی از کلاس‌های منطق بنویسید. همین چهار حرکت، در هفته اول، افزونه را یک پله از سطح آماتور بالا می‌برد.

تجربه شما از ساختاردهی افزونه چیست؟ آیا در پروژه‌ای ساختار بد را با هزینه کم بازسازی کرده‌اید یا مجبور شده‌اید به بازنویسی کامل تن بدهید؟ آن تجربه، برای خواننده بعدی که همین امروز پشت این تصمیم ایستاده، از هر راهنمای دیگری کاربردی‌تر است. 🛠️