2019年9月22日 星期日

Symfony4.3 基本應用【第三篇】主控台指令 Console Commands


Symfony透過bin/console提供許多指令(e.g. 常見的指令bin/console cache:clear),這些指令是由Console component建立,你可以使用它來建立新指令。


The Console: APP_ENV & APP_DEBUG

檔案.env裡的變數APP_ENV定義了主控台指令所運行的環境(environment),預設為 dev ,另外也會讀取 APP_DEBUG 的值來決定是否開啟或關閉"除錯(debug)"模式(預設 1 是開啟)。

若要在其他環境跑指令或是除錯模式,可以編輯 APP_ENVAPP_DEBUG 的值。


建立指令

指令定義在一個延展(extned)Command的class。例如,你可能想要有個指令可以建立使用者:
// src/Command/CreateUserCommand.php
namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class CreateUserCommand extends Command
{
    // the name of the command (the part after "bin/console")
    protected static $defaultName = 'app:create-user';

    protected function configure()
    {
        // ...
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        // ...
    }
}


設定指令

你可以選擇要不要定義敘述(description)、提示語和輸入選項及變數(input options and arguments):
// ...
protected function configure()
{
    $this
        // the short description shown while running "php bin/console list"
        ->setDescription('Creates a new user.')

        // the full command description shown when running the command with
        // the "--help" option
        ->setHelp('This command allows you to create a user...')
    ;
}
在指令的建構裡,最後會自動呼叫configure(),如果你的指令定義在自己的建構函數裡,先設定屬性並呼叫上層的建構函數(constructor),讓它的屬性可以在configure()裡被使用。
// ...
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;

class CreateUserCommand extends Command
{
    // ...

    public function __construct(bool $requirePassword = false)
    {
        // best practices recommend to call the parent constructor first and
        // then set your own properties. That wouldn't work in this case
        // because configure() needs the properties set in this constructor
        $this->requirePassword = $requirePassword;

        parent::__construct();
    }

    protected function configure()
    {
        $this
            // ...
            ->addArgument('password', $this->requirePassword ? InputArgument::REQUIRED : InputArgument::OPTIONAL, 'User password')
        ;
    }
}


註冊指令

Symfony指令必須註冊成為一個服務(serviece)並標記(tagged)上標籤console.command,如果你使用預設的service.yaml設定檔(default service.yaml configuration),它已經幫你把這些事處理好了,真是多虧了autoconfiguration


執行指令

設定並註冊指令之後,你可以在終端機裡執行:
php bin/console app:create-user
就像你預期的一樣,這個指令並不會執行任何事情,因為你根本還沒寫任何程式邏輯在內,請把這些寫到 execute() 裡面。


主控台輸出(Console Output)

execute()方法可以存取輸出端,將訊息呈現在主控台上。
// ...
protected function execute(InputInterface $input, OutputInterface $output)
{
    // outputs multiple lines to the console (adding "\n" at the end of each line)
    $output->writeln([
        'User Creator',
        '============',
        '',
    ]);

    // the value returned by someMethod() can be an iterator (https://secure.php.net/iterator)
    // that generates and returns the messages with the 'yield' PHP keyword
    $output->writeln($this->someMethod());

    // outputs a message followed by a "\n"
    $output->writeln('Whoa!');

    // outputs a message without adding a "\n" at the end of the line
    $output->write('You are about to ');
    $output->write('create a user.');
}
現在請試著執行指令:
php bin/console app:create-user
User Creator
============

Whoa!
You are about to create a user.


輸出區塊(Output Sections)

一般主控台的輸入可以分為多個獨立的區塊(section),稱為"output sections",當你需要清除或是覆蓋掉輸出的資訊,可以建立一個或是多個區塊。

section()會回傳一個ConsoleSectionOutput實例(instance),用來建立區塊:
class MyCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $section1 = $output->section();
        $section2 = $output->section();
        $section1->writeln('Hello');
        $section2->writeln('World!');
        // Output displays "Hello\nWorld!\n"

        // overwrite() replaces all the existing section contents with the given content
        $section1->overwrite('Goodbye');
        // Output now displays "Goodbye\nWorld!\n"

        // clear() deletes all the section contents...
        $section2->clear();
        // Output now displays "Goodbye\n"

        // ...but you can also delete a given number of lines
        // (this example deletes the last two lines of the section)
        $section1->clear(2);
        // Output is now completely empty!
    }
}
每個區塊的資訊會自動換行。

輸出區塊可以讓你以更進階的方式操控主控台輸出,像是呈現多個進度條(displaying multiple progress bars),能夠獨立地更新;以及加入多列(rows)到已經呈現的表格中。


主控台輸入(Console Input)

使用輸入選項或變數來傳遞訊息給指令:
use Symfony\Component\Console\Input\InputArgument;

// ...
protected function configure()
{
    $this
        // configure an argument
        ->addArgument('username', InputArgument::REQUIRED, 'The username of the user.')
        // ...
    ;
}

// ...
public function execute(InputInterface $input, OutputInterface $output)
{
    $output->writeln([
        'User Creator',
        '============',
        '',
    ]);

    // retrieve the argument value using getArgument()
    $output->writeln('Username: '.$input->getArgument('username'));
}
現在,你可以在指令裡傳入使用者名稱:
php bin/console app:create-user Wouter
User Creator
============

Username: Wouter
請看Console Input (Arguments & Options)了解更多關於輸入選項或變數的訊息。


從服務容器中取得服務(Getting Services from the Service Container)

為了要真正建立新的使用者,指令必須存取一些服務(services),因為指令已經註冊成服務,所以你可以使用一般的依賴注入(dependency injection),想像一下你打算存取一個服務 App\Service\UserManager
// ...
use App\Service\UserManager;
use Symfony\Component\Console\Command\Command;

class CreateUserCommand extends Command
{
    private $userManager;

    public function __construct(UserManager $userManager)
    {
        $this->userManager = $userManager;

        parent::__construct();
    }

    // ...

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        // ...

        $this->userManager->create($input->getArgument('username'));

        $output->writeln('User successfully generated!');
    }
}


指令的生命週期

運行指令時有三個生命週期方法可以調用(invoked):
initialize()(optional)
這個方法會在interact()execute()之前執行,主要的目的是初始化在其他指令方法中使用的變數。

interact()(optional)
這個方法在initialize()之後、execute()之前執行,目的是確認某些選項或變數是否遺漏,以及互動式地詢問使用者那些值,這是最後一次確認遺漏的選項或變數,若經過這個步驟仍有缺少的選項或變數,會導致錯誤發生。

execute()(required)
這個方法在interact()initialize()之後執行,它包含所有指令程式邏輯。


測試指令

Symfony提供一些工具幫助你測試指令,最有用的是一個叫CommandTester的class,它使用特殊的輸入和輸出classes,無需實際控制台即可輕鬆進行測試:
// tests/Command/CreateUserCommandTest.php
namespace App\Tests\Command;

use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;

class CreateUserCommandTest extends KernelTestCase
{
    public function testExecute()
    {
        $kernel = static::createKernel();
        $application = new Application($kernel);

        $command = $application->find('app:create-user');
        $commandTester = new CommandTester($command);
        $commandTester->execute([
            'command'  => $command->getName(),

            // pass arguments to the helper
            'username' => 'Wouter',

            // prefix the key with two dashes when passing options,
            // e.g: '--some-option' => 'option_value',
        ]);

        // the output of the command in the console
        $output = $commandTester->getDisplay();
        $this->assertContains('Username: Wouter', $output);

        // ...
    }
}
你可以透過ApplicationTester測試整個主控台應用程式。

當你在獨立的專案使用Console元件,請用Symfony\Component\Console\Application並且延展\PHPUnit\Framework\TestCase


紀錄指令錯誤(Logging Command Errors)

運行指令時若拋出任何例外(exception),Symfony會包含整個失敗的指令新增一筆紀錄。此外,Symfony註冊事件訂閱者(event subscriber)來監聽ConsoleEvents::TERMINATE event,以及每當指令並沒有以狀態0成功離開時,記錄這些訊息。


資料翻譯自:
https://symfony.com/doc/4.3/console.html

系列文章:


沒有留言:

張貼留言