پلاگین کنسول - اجرای فایلی از دستورات SQL

مقدمه

این مثال مفاهیم شرح داده شده در پلاگین پایه‌ی "HelloWorld" کنسول را گسترش می‌دهد تا موارد زیر را شامل شود:

- گزینه‌های پلاگین

- تعریف یک آرگومان در خط فرمان

- تعریف یک گزینه در خط فرمان

- استفاده از گزینه‌های استاندارد خط فرمان جوملا

این مثال استفاده از چندین متد از کلاس Joomla\Console\Command\AbstractCommand را توضیح می‌دهد.

عملکرد

این پلاگین کنسول، یک ابزار خط فرمان به نام `sql:execute-file` را فعال می‌کند که به وسیله‌ی آن می‌توان یک سری دستورات SQL داخل یک فایل را اجرا کرد، در حالی که دستورات شامل پیشوند جداول جوملا هستند، مانند:

 
CREATE TABLE IF NOT EXISTS `#__temp_table` (
  `s` VARCHAR(255) NOT NULL DEFAULT '',
  `i` INT NOT NULL DEFAULT 1,
  PRIMARY KEY (`i`)
);
INSERT INTO `#__temp_table` (`s`, `i`) VALUES ('Hello', 22),('there', 23);

 

نام فایل باید به صورت آرگومان در خط فرمان وارد شود:

php cli/joomla.php sql:execute-file sqlfile.sql

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

در نهایت، یک گزینه پلاگین داریم که مشخص می‌کند آیا کنترل تراکنش روی دستورات اعمال شود یا خیر.

برای سادگی، فقط از زبان انگلیسی استفاده می‌کنیم؛ برای دیدن نحوه ایجاد پلاگین چندزبانه به "Basic Content Plugin" مراجعه کنید.

طراحی کلی

مانند پلاگین کنسول پایه، دو کلاس اصلی وجود دارد:

- یک کلاس پلاگین کنسول که جنبه‌های مرتبط با مکانیزم پلاگین جوملا را مدیریت می‌کند

- یک کلاس فرمان که کد مربوط به فرمان را شامل می‌شود

کلاس پلاگین کنسول

هسته کلاس پلاگین ما به این شکل است:

 
class SqlfileConsolePlugin extends CMSPlugin implements SubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            \Joomla\Application\ApplicationEvents::BEFORE_EXECUTE => 'registerCommands',
        ];
    }

    public function registerCommands(): void
    {
        $myCommand = new RunSqlfileCommand();
        $myCommand->setParams($this->params);
        $this->getApplication()->addCommand($myCommand);
    }
}

وقتی پلاگین ما راه‌اندازی می‌شود، جوملا متد `getSubscribedEvents()` را برای فهمیدن اینکه چه رویدادهای پلاگین‌ای می‌خواهیم مدیریت کنیم، فراخوانی می‌کند. پاسخ ما به جوملا می‌گوید هنگام وقوع رویداد `ApplicationEvents::BEFORE_EXECUTE`، متد `registerCommands()` را فراخوانی کند.

درون `registerCommands()` سه کار انجام می‌دهیم:

- نمونه‌ای از کلاس فرمان خود ایجاد می‌کنیم

- پارامترهای پلاگین را به کلاس فرمان تزریق می‌کنیم (که کمی بعد دقیق‌تر بررسی می‌کنیم)

- نمونه کلاس فرمان را به اپلیکیشن کنسول اصلی جوملا اضافه می‌کنیم

پارامترهای پلاگین

یک پارامتر قابل تنظیم برای پلاگین تعریف می‌کنیم با افزودن این بخش به فایل مانیفست:

 
<config>
    <fields name="params">
        <fieldset name="basic">
            <field
                name="txn"
                type="list"
                label="Use Transaction Control?"
                default="1" >
                <option value="0">NO</option>
                <option value="1">YES</option>
            </field>
        </fieldset>
    </fields>
</config>

وقتی پلاگین نصب شد، می‌توانیم در بخش مدیریت به مسیر System / Plugins برویم، روی پلاگین‌ی کنسول "Execute SQL file" کلیک کنیم و گزینه فعال یا غیرفعال کردن کنترل تراکنش را ببینیم.

چون پلاگین ما از `Joomla\CMS\Plugin\CMSPlugin` ارث‌برده است، مقادیر پارامترها از طریق `$this->params` قابل دسترسی است. ما می‌خواهیم از این مقدار داخل کلاس فرمان استفاده کنیم، پس در کلاس فرمان متدهای مشخص کننده (`setParams()`) و دریافت‌کننده (`getParams()`) پارامتر را تعریف می‌کنیم و پارامترها را به این کلاس تزریق می‌کنیم به شکل:

 
$myCommand->setParams($this->params);

سپس داخل کلاس فرمان مقدار مورد نظر را اینگونه می‌خوانیم:

 
$transactionControl = $this->getParams()->get('txn', 1);

رشته `'txn'` باید دقیقا با مقدار بخش `name` فیلد در قسمت `<config>` فایل مانیفست مطابقت داشته باشد.

کلاس فرمان (Command Class)

Command Class از `Joomla\Console\Command\AbstractCommand` ارث می‌برد و APIهای مرتبط با این کلاس در مستندات API جوملا فهرست شده‌اند. در پلاگین پایه Helloworld Console Plugin قبلاً تعدادی از این APIها استفاده شده بود و در اینجا چند مورد دیگر را بررسی می‌کنیم.

تعریف آرگومان

شما در متد `configure()` کلاس فرمان خود مشخص می‌کنید که چه آرگومان‌هایی برای فرمان می‌خواهید. برای تعریف یک آرگومان، به صورت نمونه از کد زیر استفاده می‌شود:

 
$this->addArgument('sqlfile', InputArgument::REQUIRED, 'file of joomla sql commands', null);

پارامترها عبارتند از:

- نام آرگومان — که برای دریافت مقدار آن استفاده خواهید کرد

- مقدار `InputArgument::REQUIRED` یا `InputArgument::OPTIONAL` که در کلاس زیر تعریف شده اند:

`Symfony\Component\Console\Input\InputArgument`

- توضیح آرگومان — که هنگام نمایش help با دستور زیر دیده می‌شود:

php cli/joomla.cli sql:execute-file -h

- مقدار پیش‌فرض آرگومان (اگر اختیاری باشد)

زمانی که فرمان اجرا می‌شود، مقدار آرگومان را اینطور دریافت می‌کنید:

 
protected function doExecute(InputInterface $input, OutputInterface $output): int
{
    $sqlfile = $input->getArgument('sqlfile');
    ...
}

 

تعریف گزینه (Option)

گزینه‌ها نیز در متد `configure()` تعریف می‌شوند، مثلاً:

 
$this->addOption('logfile', "l", InputOption::VALUE_REQUIRED, "log file");

پارامترها عبارتند از:

- نام گزینه — که برای دریافت مقدار آن استفاده می‌شود

- میانبر — به کاربر اجازه می‌دهد به جای `--logfile` از `-l` استفاده کند

- نوع مقدار (`Mode`) — یکی از مقادیر ممکن در کلاس `Symfony\Component\Console\Input\InputOption`

- توضیح گزینه — هنگام نمایش help نشان داده می‌شود

- مقدار پیش‌فرض (در صورت وجود)

مقدار گزینه داخل متد `doExecute()` با این کد قابل دریافت است:

 
$logging = $input->getOption("logfile");

اگر می‌خواهید امکان استفاده از فرم `--logfile=log.txt` (با علامت مساوی) هم باشد، باید علامت مساوی را حذف کنید، مثلاً با:

 
$logfile = $logpath . '/' . ltrim($logging, "=");

در این کد، متغیر `$logpath` مسیر پوشه لاگ را از پارامتر "Path to Log Folder" در تنظیمات عمومی جوملا دریافت می‌کند.

استفاده از گزینه‌های تعریف شده توسط جوملا

جوملا گزینه `help` را ارائه می‌دهد که متن راهنما را نمایش می‌دهد:

php cli/joomla.cli sql:execute-file -h

کافی است متن راهنما را در متد `configure()` با:

$this->setHelp(...);

تنظیم کنید.

متن راهنما استاندارد همچنین گزینه‌های دیگری که جوملا از پیش تعریف کرده است را نشان می‌دهد، پس فقط باید در `doExecute()` مقدارهای آن‌ها را بگیرید، مثل:

 
$verbose = $input->getOption('verbose');
 

متد getSynopsis()

این متد یک رشته متنی است که نحوه استفاده از فرمان را توضیح می‌دهد. می‌توانید نسخه کوتاه یا بلند آن را بدست آورید:

 
$shortSynopsis = $this->getSynopsis(true);
$longSynopsis = $this->getSynopsis(false);

در این پلاگین نمونه، استفاده این متد در متن راهنما در `setHelp()` گنجانده شده است، بنابراین هنگام اجرای دستور help مانند:

php cli/joomla.cli sql:execute-file -h

قابل مشاهده است و دقیقاً با بخش "Usage:" در بالای نمایش راهنما همخوانی دارد.

کد پلاگین

این بخش شامل کد کامل منبع پلاگین کنسول است. می‌توانید این پلاگین را با کپی کردن کد زیر به صورت دستی بنویسید، یا فایل zip آن را از لینک «Download Console Plugin Sqlfile» دریافت کنید. اگر پلاگین را دستی می‌نویسید، فایل‌ها را در پوشه‌ای مانند `plg_console_sqlfile` قرار دهید.

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

فایل مانیفست 

`plg_console_sqlfile/sqlfile_cli.xml`

 
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="console" method="upgrade">
    <name>Execute SQL file console command</name>
    <version>1.0.0</version>
    <creationDate>today</creationDate>
    <author>Me</author>
    <description>Executes a file of SQL commands (eg for an upgrade)</description>
    <namespace path="src">My\Plugin\Console\Sqlfile</namespace>
    <files>
        <folder plugin="sqlfile_cli">services</folder>
        <folder>src</folder>
    </files>
    <config>
        <fields name="params">
            <fieldset name="basic">
                <field
                    name="txn"
                    type="list"
                    label="Use Transaction Control?"
                    default="1"
                >
                    <option value="0">JNO</option>
                    <option value="1">JYES</option>
                </field>
            </fieldset>
        </fields>
    </config>
</extension>

 

فایل Service Provider 

`plg_console_sqlfile/services/provider.php`

این فایل شامل کد استاندارد برای ثبت پلاگین در دیپن던سی اینجکشِن (Dependency Injection Container) است. کافیست خطوط مرتبط با پلاگین خود را به درستی تنظیم کنید. اینجا اپلیکیشن هم به کد پلاگین تزریق می‌شود چون در پلاگین استفاده می‌شود.

 
<?php
defined('_JEXEC') or die;

use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\CMS\Factory;
use Joomla\Event\DispatcherInterface;
use My\Plugin\Console\Sqlfile\Extension\SqlfileConsolePlugin;

return new class implements ServiceProviderInterface
{
    public function register(Container $container)
    {
        $container->set(
            PluginInterface::class,
            function (Container $container) {
                $dispatcher = $container->get(DispatcherInterface::class);
                $plugin     = new SqlfileConsolePlugin(
                    $dispatcher,
                    (array) PluginHelper::getPlugin('console', 'sqlfile_cli')
                );
                $plugin->setApplication(Factory::getApplication());

                return $plugin;
            }
        );
    }
};
 

فایل پلاگین کنسول 

`plg_console_sqlfile/src/Extension/SqlfileConsolePlugin.php`

این فایل مسئول مدیریت تعامل با چارچوب پلاگین‌های جوملا است:

 
<?php
namespace My\Plugin\Console\Sqlfile\Extension;

\defined('_JEXEC') or die;

use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;
use Joomla\Application\ApplicationEvents;
use Joomla\CMS\Factory;
use My\Plugin\Console\Sqlfile\CliCommand\RunSqlfileCommand;

class SqlfileConsolePlugin extends CMSPlugin implements SubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            \Joomla\Application\ApplicationEvents::BEFORE_EXECUTE => 'registerCommands',
        ];
    }

    public function registerCommands(): void
    {
        $myCommand = new RunSqlfileCommand();
        $myCommand->setParams($this->params);
        $this->getApplication()->addCommand($myCommand);
    }
}
 

فایل فرمان (Command file)

فایل زیر مسئول اجرای فرمان `sql:execute-file` است. 

`plg_console_sqlfile/src/CliCommand/RunSqlfileCommand.php`

 
<?php
namespace My\Plugin\Console\Sqlfile\CliCommand;

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Joomla\Console\Command\AbstractCommand;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Filesystem\File;

class RunSqlfileCommand extends AbstractCommand
{
    /**
     * نام پیش‌فرض فرمان
     *
     * @var    string
     * @since  4.0.0
     */
    protected static $defaultName = 'sql:execute-file';
    
    /**
     * پارامترهای مرتبط با پلاگین به همراه متد دسترسی (getter) و تنظیم (setter)
     * این پارامترها توسط نمونه پلاگین به این کلاس تزریق می‌شوند
     */
    protected $params;
    
    protected function getParams() {
        return $this->params;
    }
    
    public function setParams($params) {
        $this->params = $params;
    }

    /**
     * تابع داخلی برای اجرای فرمان
     *
     * @param   InputInterface   $input   ورودی فرمان
     * @param   OutputInterface  $output  خروجی فرمان
     *
     * @return  integer  کد خروجی فرمان
     *
     * @since   4.0.0
     */
    protected function doExecute(InputInterface $input, OutputInterface $output): int
    {
        $symfonyStyle = new SymfonyStyle($input, $output);

        $symfonyStyle->title('اجرا کردن فایل SQL');
        
        // گرفتن نام فایل حاوی دستورات SQL جوملا به عنوان آرگومان فرمان
        $sqlfile = $input->getArgument('sqlfile');
        if (!file_exists($sqlfile)) {
            $symfonyStyle->error("فایل {$sqlfile} وجود ندارد");
            return false;
        }
        
        // گرفتن نام فایل لاگ برای ثبت دستورات sql واقعی به عنوان گزینه فرمان
        if ($logging = $input->getOption("logfile")) {
            $config = Factory::getApplication()->getConfig();
            $logpath = Factory::getApplication()->get('log_path', JPATH_ADMINISTRATOR . '/logs');
            // ممکن است کاربر علامت = بعد از "-l" وارد کرده باشد؛ در اینصورت باید حذف شود
            $logfile = $logpath . '/' . ltrim($logging, "=");
        }
        
        // این گزینه استاندارد توسط جوملا تنظیم شده است
        $verbose = $input->getOption('verbose');
        
        // خواندن محتوای فایل sql داخل یک بافر
        $buffer = file_get_contents($sqlfile);
        if ($buffer === false) {
            $symfonyStyle->error("امکان خواندن محتوای فایل {$sqlfile} وجود ندارد");
            return false;
        }
        
        // استفاده مجدد از کد فرآیند نصب جوملا در libraries/src/Installer/Installer.php
        $queries = Installer::splitSql($buffer);
        if (\count($queries) === 0) {
            $symfonyStyle->error("هیچ دستور SQL در فایل {$sqlfile} پیدا نشد");
            return false;
        }

        $db = Factory::getContainer()->get(DatabaseInterface::class);
        
        // گرفتن پارامتر پلاگین که مشخص می‌کند کنترل تراکنش فعال باشد یا خیر
        // دقت کنید که برخی دستورات SQL مانند CREATE TABLE به صورت ضمنی commit دارند
        // کد زیر این حالت را به طور کامل مدیریت نمی‌کند
        $transactionControl = $this->getParams()->get('txn', 1);
        if ($transactionControl) {
            $db->transactionStart();
        }
        
        foreach ($queries as $query) {

            try {
                if ($verbose) {
                    $symfonyStyle->info("در حال اجرا: \n{$query}");
                }
                $db->setQuery($query)->execute();

                // جایگزینی پیشوند جدول در دستور SQL قبل از لاگ کردن
                $statement = $db->replacePrefix((string) $query);

if ($logging) {
    // اگر لاگ کردن فعال باشد، تلاش می‌کنیم دستور SQL اجرا شده را به فایل لاگ اضافه کنیم
    if (!File::append($logfile, $statement . "\n")) {
        // اگر اضافه کردن به فایل لاگ موفقیت‌آمیز نبود، استثنا پرتاب می‌کنیم
        throw new \RuntimeException('امکان نوشتن در فایل لاگ وجود ندارد.');
    }
}

// اگر حالت verbose فعال باشد، پیام موفقیت نمایش می‌دهیم
if ($verbose) {
    $symfonyStyle->success(Text::_('Success'));
}
} catch (ExecutionFailureException $e) {
    // اگر اجرای دستور SQL به خطا خورد
    if ($transactionControl) {
        // اگر کنترل تراکنش فعال است، تراکنش را برمی‌گردانیم (rollback)
        $db->transactionRollback();
        $symfonyStyle->info("Rolling back database\n");
    }
    // پیام هشدار نمایش می‌دهیم
    $symfonyStyle->warning($e->getMessage());
    
    // کد خروجی ۲ را برمی‌گردانیم (شما می‌توانید هر کد دلخواه تعین کنید)
    return 2;
}
}

// اگر کنترل تراکنش فعال است، پس از اجرای موفق تمام دستورات، تراکنش را تایید می‌کنیم (commit)
if ($transactionControl) {
    $db->transactionCommit();
}

// پیام موفقیت نهایی (تعداد کل کوئری‌های اجرا شده و نام فایل) را نمایش می‌دهیم
$symfonyStyle->success(\count($queries) . " SQL queries executed from {$sqlfile}");

return 0;
}

/**
 * پیکربندی فرمان
 *
 * @return void
 * @since 4.0.0
 */
protected function configure(): void
{
    // تعریف آرگومان اجباری 'sqlfile' برای مشخص کردن فایل دستورات SQL
    $this->addArgument('sqlfile', InputArgument::REQUIRED, 'file of joomla sql commands', null);

    // تعریف گزینه (option) 'logfile' با میانبر 'l' برای مشخص کردن فایل لاگ
    $this->addOption('logfile', "l", InputOption::VALUE_REQUIRED, "log file");

    // تعیین توضیح کوتاه برای فرمان
    $this->setDescription('Run a list of SQL commands in a file.');

    // گرفتن خلاصه کوتاه استفاده از فرمان برای نمایش در راهنما
    $shortSynopsis = $this->getSynopsis(true);

    // تعیین متن راهنما که هنگام اجرای دستور با گزینه -h نمایش داده می‌شود
    $this->setHelp(
        <<<EOF
The <info>%command.name%</info> command runs the SQL commands in the file passed as the --sqlfile argument
<info>php %command.full_name%</info>
Usage: {$shortSynopsis}
EOF
    );
}
}
 

نصب و اجرای پلاگین

1. ابتدا از پوشه پلاگین یک فایل زیپ (zip) بسازید و سپس آن را به روال معمول نصب پلاگین‌ها در جوملا نصب کنید. 

   فراموش نکنید که پلاگین را پس از نصب، فعال (enable) کنید!

2. در ترمینال (یا خط فرمان) به شاخه اصلی (top level) نصب جوملا بروید و دستور زیر را اجرا کنید:

php cli/joomla.php

در خروجی باید فرمان `sql:execute-file` را ببینید به همراه توضیحی که شما در متد `configure()` و در فراخوانی `$this->setDescription()` تعیین کرده‌اید.

3. برای مشاهده راهنمای این فرمان دستور زیر را اجرا کنید:

php cli/joomla.php sql:execute-file -h

   در اینجا پارامتر `sqlfile` و گزینه `logfile` به همراه متن راهنما (که شما در `$this->setHelp()` تنظیم کرده‌اید) نمایش داده خواهند شد.

تست پلاگین

برای تست ابتدا در شاخه `cli` در نصب جوملا یک فایل SQL بسازید، مثلاً:

`cli/test.sql`

 
CREATE TABLE IF NOT EXISTS `#__temp_table` (
  `s` VARCHAR(255) NOT NULL DEFAULT '',
  `i` INT NOT NULL DEFAULT 1,
  PRIMARY KEY (`i`)
);
INSERT INTO `#__temp_table` (`s`, `i`) VALUES ('Hello', 22),('there', 23);
 

سپس پلاگین را اینطور اجرا کنید:

php cli/joomla.php sql:execute-file cli/test.sql --logfile=test.log -v

- دستورات SQL اجرا می‌شوند و در حالت verbose (حرفه‌ای) نتیجه هر دستور (موفق یا خطا) نمایش داده می‌شود.

- فایل لاگ در پوشه لاگ جوملا (به طور پیش‌فرض `administrator/logs`) ساخته می‌شود و دستورات اجرا شده داخل آن ذخیره می‌شود.

اگر فرمان را دوباره اجرا کنید، کوئری INSERT دوم به دلیل کلید اولیه یکتا (`PRIMARY KEY`) روی ستون `i` با خطا مواجه خواهد شد.

 نکته پایانی

می‌توانید پارامتر `Transaction Control` که در تنظیمات پلاگین است را تست کنید. 

وقتی این پارامتر فعال باشد، در صورت بروز خطا کل تراکنش بازگردانده می‌شود (rollback). البته توجه داشته باشید که برخی دستورهای SQL مانند `CREATE TABLE` تراکنش‌پذیر نیستند و کنترل تراکنش روی آن‌ها تأثیر ندارد.