或许最容易想到的,是通过 system
或者 exec
里执行命令,只不过这么做显得太过粗线条对吧——系统调用函数系列不一定主机提供商允许运行,而且运行命令得重新初始化 Symfony2 框架运行环境,多浪费计算资源。
这两个问题,最需要解决的是第一个问题。为了安全性,很多环境 PHP 的系统调用系列函数都被 disable 掉了。不过这个问题也好解决,我们来看看 app/console 文件到底执行了什么就明白了。
// app/console
...
use SymfonyBundleFrameworkBundleConsoleApplication;
use SymfonyComponentConsoleInputArgvInput;
...
$input = new ArgvInput();
...
$kernel = new AppKernel($env, $debug);
$application = new Application($kernel);
$application->run($input);
原来就是新建了一个 Application
对象并注入了 $kernel
就行了啊……且慢,输入的参数是怎么传入命令的呢?我们再看看 Symfony\Component\Console\Input\ArgvInput
类,看能不能发现什么:
// vendor/symfony/symfony/src/Symfony/Component/Console/Input/ArgInput.php
...
class ArgvInput extends Input
{
...
public function __construct(array $argv = null, InputDefinition $definition = null)
{
if (null === $argv) {
$argv = $_SERVER['argv'];
}
// strip the application name
array_shift($argv);
$this->tokens = $argv;
parent::__construct($definition);
}
...
}
原来如此,ArgvInput
在构建时,如果没有输入第一个参数,那么会自动采用 $_SERVER['argv']
(其实也就是 $argv
变量)作为传入的参数。
当我们在命令行调用一个 PHP 脚本的时候,$argv
是如下的样子:
$ php app/console foo:bar --foo
$argv = array(
0 => 'app/console',
1 => 'foo:bar',
3 => '--foo',
)
所以才会有 array_shift($argv)
一句,把 app/console
从 $argv
里除掉,后面的处理并不需要它。
那么,我们是不是可以通过创建一个 Application
的方式来运行 Symfony2 项目里的命令了呢?当然可以!你只用创建好一个 ArgvInput
作为 Application
的第一个参数就可以了,比如调用 cache:clear
命令:
// namespace 参见 app/console 文件
$input = new ArgvInput(['app/console', 'cache:clear']);
$kernel = new AppKernel($env, $debug);
$application = new Application($kernel);
$application->run($input); // 如果你在命令行里调用命令,你还可以把 output 作为第二参数传入
不过,在已有的命令或者控制器里,不必创建 $kernel
,因为 $kernel
已经有了,通过依赖注入容器你就可以获取:
$kernel = $this->getContainer()->get('kernel');
所以,之前 $kernel = new AppKernel($env, $debug)
那一行可以直接用上面的替换了,并且因为 Kernel 里已经包含了运行环境和 debug 开关等信息,你不用担心运行环境不一致的问题。
以上的代码可以运行,但需要传一个毫无意义的 app/console
,确实有点不舒服。我们再继续深入代码,看看能不能不用 ArgvInput
作为输入参数,毕竟 Symfony Console Component 里又不止它一个Input。
我们来看看 Symfony/Component/Console/Input
目录下实现 InputInterface
接口的类有啥。除了抽象类 Input
以外,还有它们:
StringInput
看名字就能大概猜出来使用方法。此类的构造函数的第一个参数为字符串,接下来我想不用我多说了吧。需要注意的是,命令里并不需要提到 app/console
:
// 可将$input的创建替换为以下代码:
$input = new StringInput('cache:clear');
...
看起来很符合我们的需求哦。
ArrayInput
好吧,这个是数组版本的 StringInput
。没啥好说的,注意数组里除了第一个元素以外,其他的全是 key => value 形式:
// 可将 $input 的创建替换为以下代码:
$input = new ArrayInput(['cache:clear']);
// 如果后面要接参数,必须是 k/v 形式:['doctrine:schema:update', '--force' => true];
接下来的改进
话说,为什么运行命令必须得初始化一个 Application
啊?
让我们来看看 Application
到底做了些啥:
// vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Console/Application.php
...
class Application extends BaseApplication
{
...
public function __construct(KernelInterface $kernel)
{
$this->kernel = $kernel;
parent::__construct('Symfony', Kernel::VERSION.' - '.$kernel->getName().'/'.$kernel->getEnvironment().($kernel->isDebug() ? '/debug' : ''));
$this->getDefinition()->addOption(new InputOption('--shell', '-s', InputOption::VALUE_NONE, 'Launch the shell.'));
$this->getDefinition()->addOption(new InputOption('--process-isolation', null, InputOption::VALUE_NONE, 'Launch commands from shell as a separate process.'));
$this->getDefinition()->addOption(new InputOption('--env', '-e', InputOption::VALUE_REQUIRED, 'The Environment name.', $kernel->getEnvironment()));
$this->getDefinition()->addOption(new InputOption('--no-debug', null, InputOption::VALUE_NONE, 'Switches off debug mode.'));
}
...
public function doRun(InputInterface $input, OutputInterface $output)
{
$this->kernel->boot();
...
foreach ($this->all() as $command) {
if ($command instanceof ContainerAwareInterface) {
$command->setContainer($container);
}
}
$this->setDispatcher($container->get('event_dispatcher'));
...
return parent::doRun($input, $output);
}
...
}
由此可见,从 Application
创建到执行 run
方法,做了下面这些事情:
- 构造时注入了 kernel 并从 kernel 里获取了环境信息作为参数,并增了 4 个 Symfony2 命令必有的命令选项
- 运行时尝试启动 kernel 并做了一些初始化依赖的工作
且慢,代码里执行的是 run
命令,让我们来看看父类里 run
方法以及 doRun
方法做了什么:
// vendor/symfony/symfony/src/Symfony/Component/Console/Application.php
...
class Application
{
public function run(InputInterface $input = null, OutputInterface $output = null)
{
...
try {
$exitCode = $this->doRun($input, $output);
} catch (Exception $e) {
...
}
return $exitCode;
}
public function doRun(InputInterface $input, OutputInterface $output)
{
...
$exitCode = $this->doRunCommand($command, $input, $output);
...
}
protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
{
...
$event = new ConsoleCommandEvent($command, $input, $output);
$this->dispatcher->dispatch(ConsoleEvents::COMMAND, $event);
if ($event->commandShouldRun()) {
try {
$exitCode = $command->run($input, $output);
} catch (Exception $e) {
...
}
} else {
$exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED;
}
$event = new ConsoleTerminateEvent($command, $input, $output, $exitCode);
$this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event);
return $event->getExitCode();
}
...
}
嗯,结果是一大段错误处理,以及抛出两个事件。真正执行的,还是 $command->run($input, $output)
这一句。如果大家还是想自己控制意外处理,并且不需要执行事件,其实是完全可以直接创建某个命令的实例并运行他的 run
方法。
改进之后的代码:
以执行 doctrine:database:drop --force
为例:
$command = new DropDatabaseDoctrineCommand();
$command->setContainer($container); // 如果是 ContainerAwareCommand 一定得用这句
$subInput = new InputStringInput('--force'); // 注意因为我们是直接通过命令对象执行命令,所以参数中连命令名字都不需要了
$command->run($subInput, $output);
目前为止,应该是性能最优的用代码调用 Symfony2 项目命令的方式了。
提示:是否使用最后一种方式来调用命令,也得分情况:如果你调用命令是为了批处理(按顺序执行 n 个命令),使用 Application 来运行命令更适合,毕竟可能会有处理命令运行和终止事件的监听器。如果你只需要某个命令的功能,比如清空数据库,那么你最好使用最后一种方式来调用命令帮你完成任务。不过需要注意的是,有的命令是依赖 Application
的,这种情况则需要使用 $command->setApplication($application)
或者使用 Application 来运行命令,再或者,将命令注册为服务:
app_bundle.command.my_command:
class: AppBundleCommandMyCommand
tags:
- { name: console.command }
当然,最好最好的方式:去看看你想调用的命令都执行了什么代码,把它们都找出来!
最后再给个友情提示:如果不想在 console 下输出信息,可以改用 NullOutput
哦
Symfony2 命令或控制器里跑另一个命令的 N 种方法 by Chris Yue is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

写作累,服务器还越来越贵
求分担,祝愿好人一生平安
天使打赏人
4 Comments
yuansl
4月 8, 2015 在 4:50 下午大师兄 ,感谢你的分享的心得!现在国内的程序员学习这个东西资料不多,很多英文的但是也不是很全!我也是刚接触这个框架,遇到很多问题,有些情况通读books和cookbook 都无法解决问题,英文水平太差了!还好有有道翻译!希望能多分享一下,我持续关注! 带众多symfony2的学习的朋友对你说声感谢!
Chris Yue
4月 8, 2015 在 4:55 下午好嘞~只要有人看我肯定会继续写的~
books和cookbook我也在翻译(symfony.cn/docs上),不过也的确是精力有限,翻译的内容还不多。
您可以关注Symfony.cn的微博帐号
maker
3月 16, 2015 在 9:11 上午哇塞,你开始写长篇原创教程了?
Chris Yue
3月 16, 2015 在 10:13 下午别别,都是小短篇。最近在忙啥捏