或许最容易想到的,是通过system
或者exec
里执行命令,只不过这么做显得太过粗线条对吧——系统调用函数系列不一定主机提供商允许运行,而且运行命令得重新初始化Symfony2框架运行环境,多浪费计算资源。
这两个问题,最需要解决的是第一个问题。为了安全性,很多环境PHP的系统调用系列函数都被disable掉了。不过这个问题也应该好解决,我们来看看app/console文件到底执行了什么就明白了。
1 2 3 4 5 6 7 8 9 10 11 12 |
// 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
类,看能不能发现什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 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是如下的样子:
1 2 3 4 5 6 7 8 |
$ 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
命令:
1 2 3 4 5 6 7 |
// 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已经有了,通过依赖注入容器你就可以获取:
1 2 |
$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
:
1 2 3 4 5 |
// 可将$input的创建替换为以下代码: $input = new StringInput(‘cache:clear‘); ... |
看起来很符合我们的需求哦。
ArrayInput
好吧,这个是数组版本的StringInput。没啥好说的,注意数组里除了第一个元素以外,其他的全是key => value形式:
1 2 3 4 5 |
// 可将$input的创建替换为以下代码: $input = new ArrayInput([‘cache:clear‘]); // 如果后面要接参数,必须是k/v形式:[‘doctrine:schema:update‘, ‘--force‘ => true]; |
接下来的改进
话说,为什么运行命令必须得初始化一个Application
啊?
让我们来看看Application
到底做了些啥:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// 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
方法做了什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// 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
为例:
1 2 3 4 5 6 7 |
$command = new DropDatabaseDoctrineCommand(); $command->setContainer($container); // 如果是ContainerAwareCommand一定得用这句 $subInput = new InputStringInput(‘--force‘); // 注意因为我们是直接通过命令对象执行命令,所以参数中连命令名字都不需要了 $command->run($subInput, $output); |
目前为止,应该是性能最优的用代码调用Symfony2项目命令的方式了。
提示:是否使用最后一种方式来调用命令,也得分情况:如果你调用命令是为了批处理(按顺序执行n个命令),使用Application来运行命令更 适合,毕竟可能会有处理命令运行和终止事件的监听器。如果你只需要某个命令的功能,比如清空数据库,那么你最好使用最后一种方式来调用命令帮你完成任务。 不过需要注意的是,有的命令是依赖Application
的,这种情况则需要使用$command->setApplication($application)
或者使用Application来运行命令,再或者,将命令注册为服务:
1 2 3 4 5 |
app_bundle.command.my_command: class: AppBundleCommandMyCommand tags: - { name: console.command } |
当然,最好最好的方式:去看看你想调用的命令都执行了什么代码,把它们都找出来!
最后再给个友情提示:如果不想在console下输出信息,可以改用NullOutput
哦