控制台插件示例 - 执行 SQL 语句文件
简介
此示例扩展了 基本 Hello World 控制台插件 中描述的概念,以涵盖
- 插件选项
- 在命令行中定义参数
- 在命令行中定义选项
- 使用标准 Joomla 命令行选项
它描述了 Joomla\Console\Command\AbstractCommand
类的几种方法的使用。
功能
此控制台插件启用了一个名为 sql:execute-file
的 CLI 实用程序,以在一个文件中运行一系列 SQL 命令,其中命令包括 Joomla 表前缀,如
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);
其中参数为
- 参数名称 - 您将使用此名称检索参数的值
InputArgument::REQUIRED
或InputArgument::OPTIONAL
。这些在libraries/vendor/symfony/console/Input/InputArgument.php
中的Symfony\Component\Console\Input\InputArgument
类中定义。- 参数描述 - 使用以下命令显示帮助文本时,您将看到此描述:
php cli/joomla.cli sql:execute-file -h
- 参数的默认值(如果它是可选的)
执行命令时,您可以使用以下方法获取参数的值:
protected function doExecute(InputInterface $input, OutputInterface $output): int
{
$sqlfile = $input->getArgument('sqlfile');
...
定义选项
您还在 configure()
方法中定义选项,例如
$this->addOption('logfile', "l", InputOption::VALUE_REQUIRED, "log file");
其中参数为
- 选项名称 - 您将使用此名称检索选项的值
- 快捷方式 - 我们将允许用户指定“-l”作为快捷方式
- 模式 -
libraries/vendor/symfony/console/Input/InputOption.php
中的Symfony\Component\Console\Input\InputOption
中描述的可能值之一。 - 选项描述 - 使用以下命令显示帮助文本时,您将看到此描述:
php cli/joomla.cli sql:execute-file -h
- 选项的默认值
然后,您可以在 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
的文件夹中。
如 此处 所述,在开发插件时,需要确保源代码文件中的许多内容在整个过程中保持一致。该示例还包括如何使用语言文件使您的插件与语言无关。为简单起见,此控制台插件示例仅支持英语。
清单文件
<?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 行代码,以及注入应用程序,因为它在控制台插件代码中被访问。
<?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 插件框架的交互
<?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
命令的执行。
<?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 语句文件,例如
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 语句的事务控制)。