跳至主要内容
版本:5.1

控制台插件示例 - 执行 SQL 语句文件

简介

此示例扩展了 基本 Hello World 控制台插件 中描述的概念,以涵盖

  • 插件选项
  • 在命令行中定义参数
  • 在命令行中定义选项
  • 使用标准 Joomla 命令行选项

它描述了 Joomla\Console\Command\AbstractCommand 类的几种方法的使用。

功能

此控制台插件启用了一个名为 sql:execute-file 的 CLI 实用程序,以在一个文件中运行一系列 SQL 命令,其中命令包括 Joomla 表前缀,如

sqlfile.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 语句(带有转换后的前缀)记录到文件的功能,并且我们使用 Joomla 定义的 verbose 选项来确定在终端上显示什么输出。

最后,我们合并了一个插件选项,该选项控制是否将事务控制应用于一系列命令。

为简单起见,我们只在插件中使用英语;若要了解如何使您的插件支持多种语言,请查看 基本内容插件

整体设计

与基本控制台插件一样,有两个主要类

  • 一个控制台插件类,处理与 Joomla 插件机制相关的方面
  • 一个命令类,其中包含命令的代码

控制台插件类

这是我们插件类的核心

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

当我们的插件被初始化时,Joomla 将调用 getSubscribedEvents() 以了解我们想要处理哪些插件事件。我们的响应告诉 Joomla 在触发 ApplicationEvents::BEFORE_EXECUTE 事件时调用我们的 registerCommands() 函数。

registerCommands() 中,我们接下来执行 3 件事

  • 实例化我们的命令类
  • 将插件参数注入到我们的命令类中(我们稍后将详细介绍)
  • 将我们的命令类实例添加到核心 Joomla 控制台应用程序中。

插件参数

我们通过在清单文件中包含以下内容来定义插件的可配置参数

<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>

然后,当插件安装后,我们可以在后端导航到系统/插件,点击我们的 Execute SQL file console command 插件,我们将看到设置事务控制打开或关闭的选项。

因为我们的插件扩展了 Joomla\CMS\Plugin\CMSPlugin,所以参数的值可以通过 $this->params 获得。我们希望在我们的命令类中使用该值,因此我们在那里定义了一个 setter setParams() 和 getter getParams(),并将参数通过以下方式注入到类中:

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

然后在我们的命令类中,我们可以使用以下方法获取值:

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

字符串“txn”必须与我们在清单文件中的 <config> 部分中字段的 name 属性匹配。

命令类

命令类扩展了 Joomla\Console\Command\AbstractCommand,并且与此类关联的 API 列在 API 文档 中。我们在 基本 Hello World 控制台插件 中使用了一些这些 API,在这里我们探讨了更多内容。

定义参数

您在命令类的 configure() 方法中定义您希望命令具有的参数。若要定义参数,您可以使用例如

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

其中参数为

  1. 参数名称 - 您将使用此名称检索参数的值
  2. InputArgument::REQUIREDInputArgument::OPTIONAL。这些在 libraries/vendor/symfony/console/Input/InputArgument.php 中的 Symfony\Component\Console\Input\InputArgument 类中定义。
  3. 参数描述 - 使用以下命令显示帮助文本时,您将看到此描述:
php cli/joomla.cli sql:execute-file -h
  1. 参数的默认值(如果它是可选的)

执行命令时,您可以使用以下方法获取参数的值:

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

定义选项

您还在 configure() 方法中定义选项,例如

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

其中参数为

  1. 选项名称 - 您将使用此名称检索选项的值
  2. 快捷方式 - 我们将允许用户指定“-l”作为快捷方式
  3. 模式 - libraries/vendor/symfony/console/Input/InputOption.php 中的 Symfony\Component\Console\Input\InputOption 中描述的可能值之一。
  4. 选项描述 - 使用以下命令显示帮助文本时,您将看到此描述:
php cli/joomla.cli sql:execute-file -h
  1. 选项的默认值

然后,您可以在 doExecute() 中检索选项的值

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

如果您想允许使用 --logfile=log.txt(即使用等号而不是空格)定义选项的形式,则需要使用例如 ltrim 删除等号

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

插件中的代码将 $logpath 设置为全局配置日志/"日志文件夹路径"参数。

使用 Joomla 定义的选项

Joomla 提供了一个“help”选项,使您可以显示帮助文本

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

您只需要在 configure() 方法中提供帮助文本

$this->setHelp(...);

标准帮助文本还显示了 Joomla 为您预定义的许多其他选项,因此您只需在 doExecute() 中获取其值,例如

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

getSynopsis()

此函数返回一个字符串,解释命令的使用方法,您可以获取简短或完整版本

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

在此示例插件中,它已包含在 setHelp() 中的帮助文本中,因此您可以在执行以下操作时看到它:

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

请注意它如何与帮助输出顶部“用法:”部分匹配。

插件代码

本节包含控制台插件的完整源代码。您可以通过复制下面的代码手动编写插件,也可以从 下载控制台插件 Sqlfile 下载 zip 文件。如果您要手动编写它,请将以下文件包含在例如 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>

服务提供程序文件

services/provider.php 文件是通过依赖注入容器实例化插件的相当标准的样板代码;您只需要正确编写与您的插件相关的 3 行代码,以及注入应用程序,因为它在控制台插件代码中被访问。

plg_console_sqlfile/services/provider.php
<?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
{
/**
* Registers the service provider with a DI container.
*
* @param Container $container The DI container.
*
* @return void
*
* @since 4.2.0
*/
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;
}
);
}
};

控制台插件文件

以下文件处理与 Joomla 插件框架的交互

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

命令文件

以下文件处理 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
{
/**
* The default command name
*
* @var string
* @since 4.0.0
*/
protected static $defaultName = 'sql:execute-file';

/**
* The params associated with the plugin, plus getter and setter
* These are injected into this class by the plugin instance
*/
protected $params;

protected function getParams() {
return $this->params;
}

public function setParams($params) {
$this->params = $params;
}

/**
* Internal function to execute the command.
*
* @param InputInterface $input The input to inject into the command.
* @param OutputInterface $output The output to inject into the command.
*
* @return integer The command exit code
*
* @since 4.0.0
*/
protected function doExecute(InputInterface $input, OutputInterface $output): int
{
$symfonyStyle = new SymfonyStyle($input, $output);

$symfonyStyle->title('Run a SQL file');

// get the file of joomla sql statements, as an argument to the command
$sqlfile = $input->getArgument('sqlfile');
if (!file_exists($sqlfile)) {
$symfonyStyle->error("{$sqlfile} does not exist");
return false;
}

// get the file to log the actual sql statements, as an option to the command
if ($logging = $input->getOption("logfile")) {
$config = Factory::getApplication()->getConfig();
$logpath = Factory::getApplication()->get('log_path', JPATH_ADMINISTRATOR . '/logs');
// some users might enter an = after the "-l" option; if so we need to remove it
$logfile = $logpath . '/' . ltrim($logging, "=");
}

// this is a standard option configured by Joomla
$verbose = $input->getOption('verbose');

// read the sql file into a buffer
$buffer = file_get_contents($sqlfile);
if ($buffer === false) {
$symfonyStyle->error("Could not read contents of {$sqlfile}");
return false;
}

// We reuse code from the Joomla install process in libraries/src/Installer/Installer.php
$queries = Installer::splitSql($buffer);
if (\count($queries) === 0) {
$symfonyStyle->error("No SQL queries found in {$sqlfile}");
return false;
}

$db = Factory::getContainer()->get(DatabaseInterface::class);

// Get the plugin param defining whether we should use transaction control or not
// Of course, some sql statements such as CREATE TABLE have implicit commits;
// the code below doesn't really handle that situation.
$transactionControl = $this->getParams()->get('txn', 1);
if ($transactionControl) {
$db->transactionStart();
}

foreach ($queries as $query) {

try {
if ($verbose) {
$symfonyStyle->info("Executing: \n{$query}");
}
$db->setQuery($query)->execute();
$statement = $db->replacePrefix((string) $query);
if ($logging) {
if (!File::append($logfile, $statement . "\n")) {
throw new \RuntimeException('Cannot write to log file.');
}
}
if ($verbose) {
$symfonyStyle->success(Text::_('Success'));
}
} catch (ExecutionFailureException $e) {
if ($transactionControl) {
$db->transactionRollback();
$symfonyStyle->info("Rolling back database\n");
}
$symfonyStyle->warning($e->getMessage());
return 2; // or whatever error code you want to set
}
}
if ($transactionControl) {
$db->transactionCommit();
}
$symfonyStyle->success(\count($queries) . " SQL queries executed from {$sqlfile}");

return 0;
}

/**
* Configure the command.
*
* @return void
*
* @since 4.0.0
*/
protected function configure(): void
{
$this->addArgument('sqlfile', InputArgument::REQUIRED, 'file of joomla sql commands', null);
$this->addOption('logfile', "l", InputOption::VALUE_REQUIRED, "log file");
$this->setDescription('Run a list of SQL commands in a file.');
$shortSynopsis = $this->getSynopsis(true);
$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
);
}
}

安装和执行

从文件夹生成一个 zip 文件,并以通常的方式安装插件。请记住启用插件!

然后在终端会话中导航到 Joomla 实例的顶级,并输入

php cli/joomla.php 

您应该看到列出的 sql:execute-file,以及您在命令类 configure() 方法中的 $this->setDescription() 调用中指定的描述性文本。

如果您列出帮助文本

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

那么您应该会看到您的 sqlfile 参数和您的日志记录选项,以及在 configure() 方法中的 $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 语句应该会显示,以及每个语句的成功或失败。日志文件将在 Joomla 实例的日志文件夹中创建,默认情况下为 administrator/logs。

如果您再次运行该命令,则 SQL INSERT 语句将失败,因为 i 列上存在唯一索引。

您还可以尝试设置事务控制参数(例如),以检查错误发生时数据库的回滚情况(尽管数据库不提供对 SQL CREATE TABLE 语句的事务控制)。