Symfony console error

Дата обновления перевода 2023-01-19

Как оформить консольную команду

Дата обновления перевода 2023-01-19

Как оформить консольную команду

Одним из наиболее скучных занятий при создании консольных команд является офомрление
ввода и вывода команды. Отображение заголовков и таблиц или вопросов к пользователю
задействует большое количество повторяющегося кода.

К примеру, рассмотрите код, используемый для отображение заголовка следующей команды:

Отображение простого заголовка требует трёх строчек кода: чтобы изменить фронт
контроллер, подчеркнуть содержимое и оставить дополнительную пустую строку после
заглавия. Оформление требуется для всех хорошо продуманных команд, но оно излишне
усложняет код.

Для того, чтобы уменьшить этот шаблонный код, команды Symfony могут по желанию
использовать Справочник стилей Symfony. Эти стили реализуются, как набор
методов-помощников, которые позволяют создавать семантические команды и забыть
об их оформлении.

Базовое использование

В вашей команде, инстанциируйте класс SymfonyStyle
и передайте переменные $input и $output в качестве его аргументов. Далее,
вы можете начать использовать любой из его помощников, как, например, title(),
который отображает заглавие команды:

Методы-помощники

Класс SymfonyStyle определяет некоторые
методы-помощники, которые охватывают наиболее распространённый действия, выполняемые
консольными командами.

Методы титрования

title()

Отображает заданную строку, как заглавие команды. Этот метод должен был бы
использваться в данной команде только единожды, но ничто не останавливает вас
от его повторного использования:

section()

Отображает заданную строку, как заглавие некоторой части команды. Он необходим
только в сложных командах, которые требуют лучшего разделения своего содержимого:

Методы содержания

text()

Отображает заданную строку или массив строк, как обычный текст. Это полезно
для отображения сообщений помощи и инструкций для пользователя, выполняющего
команду:

listing()

Отображает неупорядоченный список элементов, переданных в виде массива:

table()

Отображает заданный массив заголовков и строк в виде компактной таблицы:

horizontalTable()

Отображает заданный массив заголовков и строк в виде компактной горизонтальной таблицы:

definitionList()

Отображает заданные пары key => value в виде компактного списка элементов:

createTable()
Создает экземпляр Table,
оформленный в соответствии с Руководством Symfony об оформлении, что
позволяет вам использовать функции вроде добавления строк динамически.
newLine()

Отображает пустую строчку в выводе команды. Несмотря на то, что это может выглядеть
полезным, в большинстве случаев, он не понадобится вам вообще. Причина кроется в том,
что каждый помощник уже добавляет собственные пустые строки, так что вам не придётся
заботиться о вертикальной разбивке:

Методы предупреждения

note()

Отображает заданную строку или массив строк, как выделенное предупреждение.
Используйте этого помощника осторожно, чтобы избежать засорения вывода команды:

caution()

Схож с помощником note(), но содержимое выделяется более заметно.
Результирующее содержимое имеет сходство с сообщенем об ошибке,так что
вам стоит избегать использования этого помощника кроме случаев, когда
это крайне необходимо:

Методы индикатора выполнения

progressStart()

Отображает индикатор выполнения с количеством шагов, равным аргументу, переданному
методу (не передавайте значение, если длина индикатора выполнения неизвестна):

progressAdvance()

Заставляет индикатор выполнения продвинуться на заданное количество шагов
(или 1 шаг, если не было передано аргументов):

progressFinish()

Завершает индикатор выполнения (заполняет все оставшиеся шаги при неизвестной
длине):

progressIterate()

Если ваш индикатор выполнения накладывается на итерируемую коллекцию,
используйте помощника progressIterate():

createProgressBar()
Создает экземпляр ProgressBar,
оформленный в соответствии с Руководством Symfony об оформлении.

Методы ввода пользователя

ask()

Просит пользователя предоставить какие-то данные:

Вы можете передать значение по умолчанию в качестве второго аргумента, чтобы
пользователь мог просто нажать клавишу <Enter> для выбора этого значения:

В случае, если вам нужно валидировать данное значение, передайте валидатор
обратного вызова в качестве третьего аргумента:

askHidden()

Очень похож на метод ask(), но ввод пользователя будет скрыт и не может
определять значение по умолчанию. Используйте его, когда запрашиваете чувствительную
информацию:

В случае, если вам нужно валидировать заданное значение, передайте валидатор
обратного вызова в качестве второго аргумента:

confirm()

Задаёт пользователю вопрос, на который можно ответить Да/Нет и возвращает только
true или false:

Вы можете передать значение по умолчанию в качестве второго аргумента, чтобы
пользователь мог просто нажать клавишу <Enter>, чтобы выбрать это значение:

choice()

Задаёт вопрос, ответ которого ограничен заданным списком валидных ответов:

Вы можете передать значение по умолчанию в качестве третьего аргумента, чтобы
пользователь мог просто нажать клавишу <Enter>, чтобы выбрать это значение:

Наконец-то вы можете позволить пользователям выбирать несколько вариантов. Чтобы сделать
это, пользователи должны разделить каждый выбор запятой (например, введение 1, 2
выберет варианты 1 и 2):

6.2

Опция multiSelect в choice() была предсталвлена в Symfony 6.2.

Методы результатов

Note

Если вы вводите любой URL, он не будет поломан/обрезан, он будет кликабельным
— если терминал это предоставляет. Если «хорошо сформатированный вывод» важнее,
вы можете отключить это:

success()

Отображает заданную строку или массив строк, выделенных, как сообщение об успехе
(зелёный фон с ярлыком [OK]). Должен был бы использоваться единожды для отображения
финального результата выполнения данной команды, но вы можете использовать его повторно
во время выполнения команды:

info()

Похож на метод success() (заданная строка или массив строк отображаются на зеленом
фоне), но ярлык [OK] не имеет префикса, Должен быть использован единожды для
отображения финального результата выполнения заданной команды, не отображая результат
как успешный или неуспешный:

warning()

Отображает заданную строку или массив строк, выделенных, как сообщение
предостережение (с красным фоном и ярлыком [WARNING]). Должен был бы
использоваться единожды для отображения финального результата выполнения
данной команды, но вы можете использовать его повторно во время выполнения
команды:

error()

Отображает заданную строку или массив строк, выделенных, как сообщение об ошибке
(с красным фоном и ярлыком [ERROR]). Должен был бы использоваться единожды
для отображения финального результата выполнения данной команды, но вы можете
использовать его повторно во время выполнения команды:

Конфигурация стилей по умолчанию

По умолчанию, стили Symfony оборачивают всё содержание, чтобы избежать строк слишком
длинного текста. Единственным исключением являются URL, которые не оборачиваются, независимо
от их длины. Это делается, чтобы включить кликабельные URL в терминалах, которые их
поддерживают.

Если вы предпочитаете оборачивать всё содержание, включительно с URL, используйте этот
метод:

6.2

Метод setAllowCutUrls() был представлен в Symfony 6.2.

Определение ваших собственных стилей

Если вам не нравится дизайн команд, использующих Оформление Symfony, вы можете
определить ваш собственный набор стилей консоли. Просто создайте класс, реализующий
StyleInterface:

Далее, инстанциируйте в ваших командах этот пользовательский класс вместо класса
по умолчанию SymfonyStyle. Благодаря StyleInterface вам не понадобится изменять
код ваших команд, чтобы изменить их внешний вид:

Запись в вывод ошибки

Если вы повторно используете вывод команды в качестве ввода других команд, или
сбрасываете его в файл для последующего использования, вы наверное хотите исключить
индикаторы выполнения, заметки и другой вывод, который не имеет реального значения.

Команды могут выводить информацию в двух разных потоках: stdout (стандартный
вывод) — это поток, где должно выводиться настоящее содержимое, а stderr
(стандартная ошибка) — это поток, где должны выводиться ошибки и сообщения отладки.

Класс SymfonyStyle предоставляет удобный
метод под названием getErrorStyle()
для переключения между потоками. Этот метод возвращает новый экземпляр SymfonyStyle,
который использует вывод ошибок:

Note

Если вы создадите экземпляр SymfonyStyle с объектом OutputInterface, который
не является экземпляром ConsoleOutputInterface,
то метод getErrorStyle() не будет иметь никакого эффекта, а возвращённый объект
будет всё равно писать в стандартный вывод, вместо вывода ошибки.

Содержание

  1. The Console Component
  2. The Console Component
  3. Installation
  4. Creating a basic Command
  5. Coloring the Output
  6. Verbosity Levels
  7. Using Command Arguments
  8. Using Command Options
  9. Console Helpers
  10. Testing Commands
  11. Calling an existing Command
  12. Console Commands
  13. Console Commands
  14. The Console: APP_ENV & APP_DEBUG
  15. Creating a Command
  16. Configuring the Command
  17. Registering the Command
  18. Executing the Command
  19. Console Output
  20. Output Sections
  21. Console Input
  22. Getting Services from the Service Container
  23. Command Lifecycle
  24. Testing Commands
  25. Logging Command Errors
  26. Learn More
  27. Using Events
  28. The ConsoleEvents::COMMAND Event
  29. Disable Commands inside Listeners
  30. The ConsoleEvents::ERROR Event
  31. The ConsoleEvents::TERMINATE Event
  32. The ConsoleEvents::SIGNAL Event

The Console Component

Warning: You are browsing the documentation for Symfony 2.0, which is no longer maintained.

Read the updated version of this page for Symfony 6.2 (the current stable version).

The Console Component

The Console component eases the creation of beautiful and testable command line interfaces.

The Console component allows you to create command-line commands. Your console commands can be used for any recurring task, such as cronjobs, imports, or other batch jobs.

Installation

You can install the component in many different ways:

Windows does not support ANSI colors by default so the Console Component detects and disables colors where Windows does not have support. However, if Windows is not configured with an ANSI driver and your console commands invoke other scripts which emit ANSI color sequences, they will be shown as raw escape characters.

To enable ANSI colour support for Windows, please install ANSICON.

Creating a basic Command

To make a console command that greets you from the command line, create GreetCommand.php and add the following to it:

You also need to create the file to run at the command line which creates an Application and adds commands to it:

Test the new console command by running the following

This will print the following to the command line:

You can also use the —yell option to make everything uppercase:

Coloring the Output

Whenever you output text, you can surround the text with tags to color its output. For example:

It is possible to define your own styles using the class OutputFormatterStyle:

Available foreground and background colors are: black , red , green , yellow , blue , magenta , cyan and white .

And available options are: bold , underscore , blink , reverse and conceal .

You can also set these colors and options inside the tagname:

Verbosity Levels

The console has 3 levels of verbosity. These are defined in the OutputInterface:

Option Value
OutputInterface::VERBOSITY_QUIET Do not output any messages
OutputInterface::VERBOSITY_NORMAL The default verbosity level
OutputInterface::VERBOSITY_VERBOSE Increased verbosity of messages

You can specify the quiet verbosity level with the —quiet or -q option. The —verbose or -v option is used when you want an increased level of verbosity.

The full exception stacktrace is printed if the VERBOSITY_VERBOSE level is used.

It is possible to print a message in a command for only a specific verbosity level. For example:

When the quiet level is used, all output is suppressed as the default SymfonyComponentConsoleOutput::write method returns without actually printing.

Using Command Arguments

The most interesting part of the commands are the arguments and options that you can make available. Arguments are the strings — separated by spaces — that come after the command name itself. They are ordered, and can be optional or required. For example, add an optional last_name argument to the command and make the name argument required:

You now have access to a last_name argument in your command:

The command can now be used in either of the following ways:

Using Command Options

Unlike arguments, options are not ordered (meaning you can specify them in any order) and are specified with two dashes (e.g. —yell — you can also declare a one-letter shortcut that you can call with a single dash like -y ). Options are always optional, and can be setup to accept a value (e.g. dir=src ) or simply as a boolean flag without a value (e.g. yell ).

It is also possible to make an option optionally accept a value (so that —yell or yell=loud work). Options can also be configured to accept an array of values.

For example, add a new option to the command that can be used to specify how many times in a row the message should be printed:

Next, use this in the command to print the message multiple times:

Now, when you run the task, you can optionally specify a —iterations flag:

The first example will only print once, since iterations is empty and defaults to 1 (the last argument of addOption ). The second example will print five times.

Recall that options don’t care about their order. So, either of the following will work:

There are 4 option variants you can use:

Option Value
InputOption::VALUE_IS_ARRAY This option accepts multiple values (e.g. —dir=/foo —dir=/bar )
InputOption::VALUE_NONE Do not accept input for this option (e.g. —yell )
InputOption::VALUE_REQUIRED This value is required (e.g. —iterations=5 ), the option itself is still optional
InputOption::VALUE_OPTIONAL This option may or may not have a value (e.g. yell or yell=loud )

You can combine VALUE_IS_ARRAY with VALUE_REQUIRED or VALUE_OPTIONAL like this:

Console Helpers

The console component also contains a set of «helpers» — different small tools capable of helping you with different tasks:

  • Dialog Helper: interactively ask the user for information
  • Formatter Helper: customize the output colorization

Testing Commands

Symfony2 provides several tools to help you test your commands. The most useful one is the CommandTester class. It uses special input and output classes to ease testing without a real console:

The getDisplay() method returns what would have been displayed during a normal call from the console.

You can test sending arguments and options to the command by passing them as an array to the execute() method:

You can also test a whole console application by using ApplicationTester.

Calling an existing Command

If a command depends on another one being run before it, instead of asking the user to remember the order of execution, you can call it directly yourself. This is also useful if you want to create a «meta» command that just runs a bunch of other commands (for instance, all commands that need to be run when the project’s code has changed on the production servers: clearing the cache, generating Doctrine2 proxies, dumping Assetic assets, . ).

Calling a command from another one is straightforward:

First, you find() the command you want to execute by passing the command name.

Then, you need to create a new ArrayInput with the arguments and options you want to pass to the command.

Eventually, calling the run() method actually executes the command and returns the returned code from the command (return value from command’s execute() method).

Most of the time, calling a command from code that is not executed on the command line is not a good idea for several reasons. First, the command’s output is optimized for the console. But more important, you can think of a command as being like a controller; it should use the model to do something and display feedback to the user. So, instead of calling a command from the Web, refactor your code and move the logic to a new class.

Источник

Console Commands

Warning: You are browsing the documentation for Symfony 5.2, which is no longer maintained.

Read the updated version of this page for Symfony 6.2 (the current stable version).

Console Commands

The Symfony framework provides lots of commands through the bin/console script (e.g. the well-known bin/console cache:clear command). These commands are created with the Console component. You can also use it to create your own commands.

The Console: APP_ENV & APP_DEBUG

Console commands run in the environment defined in the APP_ENV variable of the .env file, which is dev by default. It also reads the APP_DEBUG value to turn «debug» mode on or off (it defaults to 1 , which is on).

To run the command in another environment or debug mode, edit the value of APP_ENV and APP_DEBUG .

Creating a Command

Commands are defined in classes extending Command. For example, you may want a command to create a user:

The Command::SUCCESS and Command::FAILURE constants were introduced in Symfony 5.1.

Configuring the Command

You can optionally define a description, help message and the input options and arguments:

The configure() method is called automatically at the end of the command constructor. If your command defines its own constructor, set the properties first and then call to the parent constructor, to make those properties available in the configure() method:

Registering the Command

Symfony commands must be registered as services and tagged with the console.command tag. If you’re using the default services.yaml configuration, this is already done for you, thanks to autoconfiguration.

Executing the Command

After configuring and registering the command, you can run it in the terminal:

As you might expect, this command will do nothing as you didn’t write any logic yet. Add your own logic inside the execute() method.

Console Output

The execute() method has access to the output stream to write messages to the console:

Now, try executing the command:

Output Sections

The regular console output can be divided into multiple independent regions called «output sections». Create one or more of these sections when you need to clear and overwrite the output information.

Sections are created with the ConsoleOutput::section() method, which returns an instance of ConsoleSectionOutput:

A new line is appended automatically when displaying information in a section.

Output sections let you manipulate the Console output in advanced ways, such as displaying multiple progress bars which are updated independently and appending rows to tables that have already been rendered.

Console Input

Use input options or arguments to pass information to the command:

Now, you can pass the username to the command:

Read Console Input (Arguments & Options) for more information about console options and arguments.

Getting Services from the Service Container

To actually create a new user, the command has to access some services. Since your command is already registered as a service, you can use normal dependency injection. Imagine you have a AppServiceUserManager service that you want to access:

Command Lifecycle

Commands have three lifecycle methods that are invoked when running the command:

initialize() (optional) This method is executed before the interact() and the execute() methods. Its main purpose is to initialize variables used in the rest of the command methods. interact() (optional) This method is executed after initialize() and before execute() . Its purpose is to check if some of the options/arguments are missing and interactively ask the user for those values. This is the last place where you can ask for missing options/arguments. After this command, missing options/arguments will result in an error. execute() (required) This method is executed after interact() and initialize() . It contains the logic you want the command to execute and it must return an integer which will be used as the command exit status.

Testing Commands

Symfony provides several tools to help you test your commands. The most useful one is the CommandTester class. It uses special input and output classes to ease testing without a real console:

If you are using a single-command application, call setAutoExit(false) on it to get the command result in CommandTester .

The setAutoExit() method for single-command applications was introduced in Symfony 5.2.

You can also test a whole console application by using ApplicationTester.

When testing commands using the CommandTester class, console events are not dispatched. If you need to test those events, use the ApplicationTester instead.

When testing commands using the ApplicationTester class, don’t forget to disable the auto exit flag:

When using the Console component in a standalone project, use Symfony\Component\Console\Application and extend the normal PHPUnitFrameworkTestCase .

Logging Command Errors

Whenever an exception is thrown while running commands, Symfony adds a log message for it including the entire failing command. In addition, Symfony registers an event subscriber to listen to the ConsoleEvents::TERMINATE event and adds a log message whenever a command doesn’t finish with the 0 exit status.

Learn More

The console component also contains a set of «helpers» — different small tools capable of helping you with different tasks:

  • Question Helper: interactively ask the user for information
  • Formatter Helper: customize the output colorization
  • Progress Bar: shows a progress bar
  • Table: displays tabular data as a table
  • Debug Formatter Helper: provides functions to output debug information when running an external program
  • Cursor Helper: allows to manipulate the cursor in the terminal

Источник

Using Events

The Application class of the Console component allows you to optionally hook into the lifecycle of a console application via events. Instead of reinventing the wheel, it uses the Symfony EventDispatcher component to do the work:

Console events are only triggered by the main command being executed. Commands called by the main command will not trigger any event.

The ConsoleEvents::COMMAND Event

Typical Purposes: Doing something before any command is run (like logging which command is going to be executed), or displaying something about the event to be executed.

Just before executing any command, the ConsoleEvents::COMMAND event is dispatched. Listeners receive a ConsoleCommandEvent event:

Disable Commands inside Listeners

Using the disableCommand() method, you can disable a command inside a listener. The application will then not execute the command, but instead will return the code 113 (defined in ConsoleCommandEvent::RETURN_CODE_DISABLED ). This code is one of the reserved exit codes for console commands that conform with the C/C++ standard:

The ConsoleEvents::ERROR Event

Typical Purposes: Handle exceptions thrown during the execution of a command.

Whenever an exception is thrown by a command, including those triggered from event listeners, the ConsoleEvents::ERROR event is dispatched. A listener can wrap or change the exception or do anything useful before the exception is thrown by the application.

Listeners receive a ConsoleErrorEvent event:

The ConsoleEvents::TERMINATE Event

Typical Purposes: To perform some cleanup actions after the command has been executed.

After the command has been executed, the ConsoleEvents::TERMINATE event is dispatched. It can be used to do any actions that need to be executed for all commands or to cleanup what you initiated in a ConsoleEvents::COMMAND listener (like sending logs, closing a database connection, sending emails, . ). A listener might also change the exit code.

Listeners receive a ConsoleTerminateEvent event:

This event is also dispatched when an exception is thrown by the command. It is then dispatched just after the ConsoleEvents::ERROR event. The exit code received in this case is the exception code.

The ConsoleEvents::SIGNAL Event

Typical Purposes: To perform some actions after the command execution was interrupted.

Signals are asynchronous notifications sent to a process in order to notify it of an event that occurred. For example, when you press Ctrl + C in a command, the operating system sends the SIGINT signal to it.

When a command is interrupted, Symfony dispatches the ConsoleEvents::SIGNAL event. Listen to this event so you can perform some actions (e.g. logging some results, cleaning some temporary files, etc.) before finishing the command execution.

Listeners receive a ConsoleSignalEvent event:

All the available signals ( SIGINT , SIGQUIT , etc.) are defined as constants of the PCNTL PHP extension.

If you use the Console component inside a Symfony application, commands can handle signals themselves. To do so, implement the SignalableCommandInterface and subscribe to one or more signals:

Источник

The Console component eases the creation of beautiful and testable command
line interfaces.

Symfony2 ships with a Console component, which allows you to create
command-line commands. Your console commands can be used for any recurring
task, such as cronjobs, imports, or other batch jobs.

Installation¶

You can install the component in many different ways:

  • Use the official Git repository (https://github.com/symfony/Console);
  • Install it via PEAR ( pear.symfony.com/Console);
  • Install it via Composer (symfony/console on Packagist).

Creating a basic Command¶

To make the console commands available automatically with Symfony2, create a
Command directory inside your bundle and create a php file suffixed with
Command.php for each command that you want to provide. For example, if you
want to extend the AcmeDemoBundle (available in the Symfony Standard
Edition) to greet us from the command line, create GreetCommand.php and
add the following to it:

// src/Acme/DemoBundle/Command/GreetCommand.php
namespace AcmeDemoBundleCommand;

use SymfonyBundleFrameworkBundleCommandContainerAwareCommand;
use SymfonyComponentConsoleInputInputArgument;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleInputInputOption;
use SymfonyComponentConsoleOutputOutputInterface;

class GreetCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            ->setName('demo:greet')
            ->setDescription('Greet someone')
            ->addArgument('name', InputArgument::OPTIONAL, 'Who do you want to greet?')
            ->addOption('yell', null, InputOption::VALUE_NONE, 'If set, the task will yell in uppercase letters')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $name = $input->getArgument('name');
        if ($name) {
            $text = 'Hello '.$name;
        } else {
            $text = 'Hello';
        }

        if ($input->getOption('yell')) {
            $text = strtoupper($text);
        }

        $output->writeln($text);
    }
}

You also need to create the file to run at the command line which creates
an Application and adds commands to it:

Test the new console command by running the following

app/console demo:greet Fabien

This will print the following to the command line:

You can also use the --yell option to make everything uppercase:

app/console demo:greet Fabien --yell

This prints:

Coloring the Output¶

Whenever you output text, you can surround the text with tags to color its
output. For example:

// green text
$output->writeln('<info>foo</info>');

// yellow text
$output->writeln('<comment>foo</comment>');

// black text on a cyan background
$output->writeln('<question>foo</question>');

// white text on a red background
$output->writeln('<error>foo</error>');

Using Command Arguments¶

The most interesting part of the commands are the arguments and options that
you can make available. Arguments are the strings — separated by spaces — that
come after the command name itself. They are ordered, and can be optional
or required. For example, add an optional last_name argument to the command
and make the name argument required:

$this
    // ...
    ->addArgument('name', InputArgument::REQUIRED, 'Who do you want to greet?')
    ->addArgument('last_name', InputArgument::OPTIONAL, 'Your last name?')
    // ...

You now have access to a last_name argument in your command:

if ($lastName = $input->getArgument('last_name')) {
    $text .= ' '.$lastName;
}

The command can now be used in either of the following ways:

app/console demo:greet Fabien
app/console demo:greet Fabien Potencier

Using Command Options¶

Unlike arguments, options are not ordered (meaning you can specify them in any
order) and are specified with two dashes (e.g. --yell — you can also
declare a one-letter shortcut that you can call with a single dash like
-y). Options are always optional, and can be setup to accept a value
(e.g. dir=src) or simply as a boolean flag without a value (e.g.
yell).

Tip

It is also possible to make an option optionally accept a value (so that
--yell or yell=loud work). Options can also be configured to
accept an array of values.

For example, add a new option to the command that can be used to specify
how many times in a row the message should be printed:

$this
    // ...
    ->addOption('iterations', null, InputOption::VALUE_REQUIRED, 'How many times should the message be printed?', 1)

Next, use this in the command to print the message multiple times:

for ($i = 0; $i < $input->getOption('iterations'); $i++) {
    $output->writeln($text);
}

Now, when you run the task, you can optionally specify a --iterations
flag:

app/console demo:greet Fabien

app/console demo:greet Fabien --iterations=5

The first example will only print once, since iterations is empty and
defaults to 1 (the last argument of addOption). The second example
will print five times.

Recall that options don’t care about their order. So, either of the following
will work:

app/console demo:greet Fabien --iterations=5 --yell
app/console demo:greet Fabien --yell --iterations=5

There are 4 option variants you can use:

Option Value
InputOption::VALUE_IS_ARRAY This option accepts multiple values
InputOption::VALUE_NONE Do not accept input for this option (e.g. --yell)
InputOption::VALUE_REQUIRED This value is required (e.g. iterations=5)
InputOption::VALUE_OPTIONAL This value is optional

You can combine VALUE_IS_ARRAY with VALUE_REQUIRED or VALUE_OPTIONAL like this:

$this
    // ...
    ->addOption('iterations', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'How many times should the message be printed?', 1)

Asking the User for Information¶

When creating commands, you have the ability to collect more information
from the user by asking him/her questions. For example, suppose you want
to confirm an action before actually executing it. Add the following to your
command:

$dialog = $this->getHelperSet()->get('dialog');
if (!$dialog->askConfirmation($output, '<question>Continue with this action?</question>', false)) {
    return;
}

In this case, the user will be asked “Continue with this action”, and unless
they answer with y, the task will stop running. The third argument to
askConfirmation is the default value to return if the user doesn’t enter
any input.

You can also ask questions with more than a simple yes/no answer. For example,
if you needed to know the name of something, you might do the following:

$dialog = $this->getHelperSet()->get('dialog');
$name = $dialog->ask($output, 'Please enter the name of the widget', 'foo');

Testing Commands¶

Symfony2 provides several tools to help you test your commands. The most
useful one is the SymfonyComponentConsoleTesterCommandTester
class. It uses special input and output classes to ease testing without a real
console:

use SymfonyComponentConsoleTesterCommandTester;
use SymfonyBundleFrameworkBundleConsoleApplication;
use AcmeDemoBundleCommandGreetCommand;

class ListCommandTest extends PHPUnit_Framework_TestCase
{
    public function testExecute()
    {
        $application = new Application();
        $application->add(new GreetCommand());

        $command = $application->find('demo:greet');
        $commandTester = new CommandTester($command);
        $commandTester->execute(array('command' => $command->getName()));

        $this->assertRegExp('/.../', $commandTester->getDisplay());

        // ...
    }
}

The :method:`Symfony\Component\Console\Tester\CommandTester::getDisplay`
method returns what would have been displayed during a normal call from the
console.

You can test sending arguments and options to the command by passing them
as an array to the :method:`Symfony\Component\Console\Tester\CommandTester::getDisplay`
method:

use SymfonyComponentConsoleTesterCommandTester;
use SymfonyBundleFrameworkBundleConsoleApplication;
use AcmeDemoBundleCommandGreetCommand;

class ListCommandTest extends PHPUnit_Framework_TestCase
{

    //--

    public function testNameIsOutput()
    {
        $application = new Application();
        $application->add(new GreetCommand());

        $command = $application->find('demo:greet');
        $commandTester = new CommandTester($command);
        $commandTester->execute(
            array('command' => $command->getName(), 'name' => 'Fabien')
        );

        $this->assertRegExp('/Fabien/', $commandTester->getDisplay());
    }
}

Tip

You can also test a whole console application by using
SymfonyComponentConsoleTesterApplicationTester.

Calling an existing Command¶

If a command depends on another one being run before it, instead of asking the
user to remember the order of execution, you can call it directly yourself.
This is also useful if you want to create a “meta” command that just runs a
bunch of other commands (for instance, all commands that need to be run when
the project’s code has changed on the production servers: clearing the cache,
generating Doctrine2 proxies, dumping Assetic assets, …).

Calling a command from another one is straightforward:

protected function execute(InputInterface $input, OutputInterface $output)
{
    $command = $this->getApplication()->find('demo:greet');

    $arguments = array(
        'command' => 'demo:greet',
        'name'    => 'Fabien',
        '--yell'  => true,
    );

    $input = new ArrayInput($arguments);
    $returnCode = $command->run($input, $output);

    // ...
}

First, you :method:`Symfony\Component\Console\Command\Command::find` the
command you want to execute by passing the command name.

Then, you need to create a new
SymfonyComponentConsoleInputArrayInput with the arguments and
options you want to pass to the command.

Eventually, calling the run() method actually executes the command and
returns the returned code from the command (0 if everything went fine, any
other integer otherwise).

Note

Most of the time, calling a command from code that is not executed on the
command line is not a good idea for several reasons. First, the command’s
output is optimized for the console. But more important, you can think of
a command as being like a controller; it should use the model to do
something and display feedback to the user. So, instead of calling a
command from the Web, refactor your code and move the logic to a new
class.

When we use Symfony’s Console component to write CLI commands in PHP (and you should!), we’re almost always writing any output to «stdout».

There’s a few ways to get general output from a CLI command using Console:

// Run the command (Laravelish)
public function fire()
{
    echo "This is sent to stdout";  // Just text

    $this->info('Some info'); // Regular Text
    $this->error('An Error'); // Red Text
}

// Run the command (Symonfyish)
public function execute(InputInterface $input, OutputInterface $output)
{
    echo "This is sent to stdout";  // Just text

    $output->writeln("<info>$string</info>");   // Regular Text
    $output->writeln("<error>$string</error>"); // Red Text
}

Console commands in Laravel use Symfony’s Console component under the hood. While I’ll write this (mostly) in context of Laravel, this is definitely applicable to Symfony users and those not using Laravel (collectively, «the haters») as well.

All of this, even the «error» output, writes to «Stdout». This isn’t necessarily good. In fact, the default behavior can easily make for some surprises to other developers calling these commands over a CLI.

Convention

The following are well established *nix conventions to follow for any CLI tool.

  • Only write pertinent, needed information to Stdout
  • Write info messages (non-pertinent) to Stderr, even if it’s not an error message
  • Only attempt to detect if the command was successful based on the result the command returns (exit status code). That will be with 0 (success) or 1 (failure).
    • Do not attempt to guess if a command failed simply because output was sent to Stderr
  • Usually if everything works, then nothing is output. Simply returning a 0 exit code is generally enough.

Writing important information to Stdout lets administrators send important data to log files. Writing non-important to Stderr lets administrators ignore it or send it to a log file specifically for errors or other information.

Perhaps more importantly is that Stdout output might get piped to another process to handle (think about anytime you do cat /some/file | grep 'search-term'). You don’t want non-important output sent to Stdout in those cases. Sending those to Stderr makes the most sense then.

Lastly, because of these conventions, it’s important that your commands return a 0 or 1 if they are successful or if the exit with an error. This is The Way™ that should be used to detect if there’s truly an error or if the command operated successfully.

In Practice

Here’s how I setup Laravel commands:

<?php namespace FooBar;

use IlluminateConsoleCommand;
use SymfonyComponentConsoleOutputConsoleOutputInterface;

class MyCommand extends Command {

    # Some boiler plate omitted

    public function run()
    {
        // Default $stdErr variable to output
        $stdErr = $this->getOutput();

        if( $this->getOutput() instanceof ConsoleOutputInterface )
        {
            // If it's available, get stdErr output
            $stdErr = $this->getOutput()->getErrorOutput();
        }

        try {
            // Some operations

            // Non-critical information message
            // Since we have the Symfony output object, use writeln function
            $stdErr->writeln('<info>Status: Working...</info>')
        } catch( Exception $e )
        {
            // Since we have the Symfony output object, use writeln function
            $stdErr->writeln('<error>'.$e->getMessage().'</error>');
            return 1;
        }

        // Important output
        $this->info('Your new API key is: aaabbbcccddd');
        return 0;
    }
}

And the same in a Symfony command:

<?php namespace FooBar;

use SymfonyComponentConsoleCommandCommand
use SymfonyComponentConsoleInputInputArgument;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleOutputConsoleOutputInterface;

class MyCommand extends Command {

    # Some boiler plate omitted

    public function execute(InputInterface $input, OutputInterface $output)
    {
        // Default $stdErr variable to output
        $stdErr = $output;

        if( $output instanceof ConsoleOutputInterface )
        {
            // If it's available, get stdErr output
            $stdErr = $output->getErrorOutput();
        }

        try {
            // Some operations

            // Non-critical information message
            $stdErr->writeln('<info>Status: Working...</info>')
        } catch( Exception $e )
        {
            $stdErr->writeln('<error>'.$e->getMessage().'</error>');
            return 1;
        }

        // Important output
        $output->writeln('<info>Your new API key is: aaabbbcccddd</info>');
        return 0;
    }
}

These classes mirror each other. The Laravel version uses some of its syntactic sugar.

Let’s go over what’s going on.

First, I assign a variable $stdErr. This gets assigned a fallback of the Output object. I’m going to use this variable later for error output regardless of whether it’s used for Stdout (the default) or Stderr.

If the Output object happens to be an instance of ConsoleOutputInterface, I’ll know it has the getErrorOutput method available. Not all Output implementations do, so this check is important. Stderr can then be assigned the Error Output object, which will write to Stderr. I can then easily differentiate output between Stderr and Stdout.

The rest of this is implementation of the above conventions. I write non-essential information to Stderr, but use the «info» formatters, as they don’t need the red error styling.

Actual errors are also output to Stderr, but with the red output styling.

Important information is output to Stdout, again with the «info» styling.

Note that I return 0 or 1 (0 for success). The return value is taken by the Symfony Console component and returned as the exit code of the command. If you define nothing, then 0 is returned, even if you have output an error!

If this command didn’t need the resulting output (for example, the new API key), I would return nothing. If I had a «success» message, I would actually return that in Stderr, but with the «info» formatting.

  1. E_USER_DEPRECATED are always silenced

So you mean by «silenced» that the PHP handler will not be run for those, took me a while to figure that one out. OK I get that.

Now, let’s consider my use case, I did explicitly told PHP to ignore E_USER_DEPRECATED, why does Symfony still log that ? Would it just be possible to honour the PHP configuration and do nothing when those errors are silenced by error_level() configuration ? Just ignoring them ?

I did some step debugging, even thought they are «silenced» meaning PHP won’t scream, they still reach that line of code:

$this->loggers[$type][0]->log($level, $logMessage, $errorAsException ? [‘exception’ => $errorAsException] : []);

So I attempted to decompose the code:

  • In __construct(), $levels is passed as null, so set to E_ALL,
  • then in configure() $handler->setDefaultLogger($this->logger, $this->levels); is called,
  • in setDefaultLogger() and setLogger() then it does a lot of black magic with levels, anyway I have a logger set for the deprecation levels before and after,
  • then when finally getting to handleError() I have a type, which is 16384 (i.e. E_USER_DEPRECATED),
  • then it checks for the PHP configuration by calling error_level() and decides to silence my error, since I disabled it ($silenced is set to true),
  • I did a slight modification, and rewrote the next line to $level |= E_RECOVERABLE_ERROR | E_USER_ERROR /* | E_DEPRECATED | E_USER_DEPRECATED */; for testing, so that the $level variable does not include the deprecation bitflags,
  • so $log is now $log = $this->loggedErrors & $type; which means E_ALL & E_USER_DEPRECATED which means it’s still E_USER_DEPRECATED (all right, everything’s fine),
  • so $throw is $this->thrownErrors & $type & $level which means that it’s not thrown because E_USER_DEPRECATED bit is not in $this->thrownErrors, so all right,
  • then $type is $type &= $level | $this->screamedErrors;, since default is E_ERROR + E_CORE_ERROR + E_COMPILE_ERROR + E_PARSE for $this->screamedErrors this lead to 0, actually, I guess in that particular line, you keeping the type if an only if one of its bits matches one of the $level bits or one of the $this->screamedErrors bits, ok, seems fine,
  • then if if (!$type || (!$log && !$throw)) { you return, good.

Now, everything in this seems fine. Except that, I did a simple patch here, I just removed the E_DEPRECATED | E_USER_DEPRECATED from the forced levels to be logged. If I restore them, end of the previous scenario will be:

  • then $type is $type &= $level | $this->screamedErrors;, since default is E_ERROR + E_CORE_ERROR + E_COMPILE_ERROR + E_PARSE for $this->screamedErrors it becomes 16384 (i.e. E_USER_DEPRECATED),
  • then if if (!$type || (!$log && !$throw)) { does not return, it executes the rest of the function, and log my error.

In here, I don’t really understand why and how logging conditions have been chosen, especially this code is very hard to read because of heavy bitflags usage, and, in my opinion, it lacks comments for the reader to understand it fully without having to debug.

From my point of view, the if (!$type || (!$log && !$throw)) {, if I understand it right, if there’s no type, then return (no type means the error was silenced by error_level() configuration), or if there’s no log nor exception, then return as well (which means that the error at the same time does not trigger exceptions, AND was not set to be logged).

Right, then I should have no $type there ! I seriously still don’t understand why:

        // Strong errors are not authorized to be silenced.
        $level |= E_RECOVERABLE_ERROR | E_USER_ERROR | E_DEPRECATED | E_USER_DEPRECATED;

I agree with the original code author «Strong errors are not authorized to be silenced». But where I do not agree, E_DEPRECATED and E_USER_DEPRECATED are not strong errors: they just are a hint that tells that something will be deprecated in the future. As a developer, I do appreciate very much to have those when I work, but as a maintainer of a production application, I’d very much like to just completely drop those from my prods.

To be honest after debugging almost the whole class (and it is very hard, because there’s more than one instance, one that restores another from the error handler and changes its internals, very hard runtime to fully understand) there is absolutely no technical reason I could see for E_DEPRECATED and E_USER_DEPRECATED to be forced. You pointed out that they are hardcodedely NOT in thrownErrors, but they also hardcodedely in loggedErrors, so either way, they will ALWAYS be logged some way or another. The only way to really silent them (I mean silent them in the logs, not silent them in the PHP error handler meaning) is to configure via the error_level() function call.

This will be a super long article, this subject is way more complex than it originally sounds. As always, I’ll be updating this soon with more info and possibly more examples as I figure it out better.

First off, you are not limited to just one logger in Symfony. However, the most popular is Monolog and Symfony has built in support making it easier to implement. So that is what I cover here.

One thing you might want to use a logger for is logging specific exceptions or errors to specific locations. You may also just want to log some value in your code for debugging purposes to see what is happening or being sent to a particular method/function. These are good use cases for logging.

While most errors/exceptions already display with lots of info in the browser, you may be like me and want certain things logged so you can review the entire pattern over a period of time.

The steps required to log sound sort of simple:

  1. Install the logger
  2. Configure the logger, decide how you want it to actually work. You don’t have to configure it with yaml. Doing so makes it so that you can autowire loggers instead of having to create a new object passing values in each time which will usually be the same values.
  3. Get the logger into your class some how( this can vary depending on whether it is a controller or a service/class)
  4. Use the logger to log something like an exception.

Installing

Your project might already have monolog logger installed since Symfony uses it by default to log everything. It doesn’t matter. Running the line below won’t hurt even if it is installed already.

To install the Monolog Logger you simply open your terminal, navigate to the root of your project and type the following :

composer require symfony/monolog-bundle

That is all. Now the monolog bundle is installed and you can use it after you configure it. And that is where the fun and confusion begin. Symfony uses Monolog so it may already be installed  depending on how you created your project.

Handlers

First decide what types of loggers you want to use. You will probably see people using the stream type logger most often. This is really generic and is the default. I don’t like the stream handler because it just keeps growing infinitely.

I prefer storing different logs to different places. You can even store the logs in a database if you want. The good news is you can use more than one type of logger, and configure them however you want.

First off, here is a long list of handler types you can use with Monolog. This is going to be SUPER LONG. I’d rather just list it here than link to a file. This list is taken from the code on github

* Possible handler types and related configurations (brackets indicate optional params):
 *
 * - service:
 *   - id
 *
 * - stream:
 *   - path: string
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [file_permission]: int|null, defaults to null (0644)
 *   - [use_locking]: bool, defaults to false
 *
 * - console:
 *   - [verbosity_levels]: level => verbosity configuration
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [console_formatter_options]: array
 *
 * - firephp:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - browser_console:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - gelf:
 *   - publisher: {id: ...} or {hostname: ..., port: ..., chunk_size: ...}
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - chromephp:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - rotating_file:
 *   - path: string
 *   - [max_files]: files to keep, defaults to zero (infinite)
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [file_permission]: string|null, defaults to null
 *   - [use_locking]: bool, defaults to false
 *   - [filename_format]: string, defaults to '{filename}-{date}'
 *   - [date_format]: string, defaults to 'Y-m-d'
 *
 * - mongo:
 *   - mongo:
 *      - id: optional if host is given
 *      - host: database host name, optional if id is given
 *      - [port]: defaults to 27017
 *      - [user]: database user name
 *      - pass: mandatory only if user is present
 *      - [database]: defaults to monolog
 *      - [collection]: defaults to logs
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - elasticsearch:
 *   - elasticsearch:
 *      - id: optional if host is given
 *      - host: elastic search host name. Do not prepend with http(s)://
 *      - [port]: defaults to 9200
 *      - [transport]: transport protocol (http by default)
 *      - [user]: elastic search user name
 *      - [password]: elastic search user password
 *   - [index]: index name, defaults to monolog
 *   - [document_type]: document_type, defaults to logs
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - redis:
 *   - redis:
 *      - id: optional if host is given
 *      - host: 127.0.0.1
 *      - password: null
 *      - port: 6379
 *      - database: 0
 *      - key_name: monolog_redis
 *
 * - predis:
 *   - redis:
 *      - id: optional if host is given
 *      - host: tcp://10.0.0.1:6379
 *      - key_name: monolog_redis
 *
 * - fingers_crossed:
 *   - handler: the wrapped handler's name
 *   - [action_level|activation_strategy]: minimum level or service id to activate the handler, defaults to WARNING
 *   - [excluded_404s]: if set, the strategy will be changed to one that excludes 404s coming from URLs matching any of those patterns
 *   - [excluded_http_codes]: if set, the strategy will be changed to one that excludes specific HTTP codes (requires Symfony Monolog bridge 4.1+)
 *   - [buffer_size]: defaults to 0 (unlimited)
 *   - [stop_buffering]: bool to disable buffering once the handler has been activated, defaults to true
 *   - [passthru_level]: level name or int value for messages to always flush, disabled by default
 *   - [bubble]: bool, defaults to true
 *
 * - filter:
 *   - handler: the wrapped handler's name
 *   - [accepted_levels]: list of levels to accept
 *   - [min_level]: minimum level to accept (only used if accepted_levels not specified)
 *   - [max_level]: maximum level to accept (only used if accepted_levels not specified)
 *   - [bubble]: bool, defaults to true
 *
 * - buffer:
 *   - handler: the wrapped handler's name
 *   - [buffer_size]: defaults to 0 (unlimited)
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [flush_on_overflow]: bool, defaults to false
 *
 * - deduplication:
 *   - handler: the wrapped handler's name
 *   - [store]: The file/path where the deduplication log should be kept, defaults to %kernel.cache_dir%/monolog_dedup_*
 *   - [deduplication_level]: The minimum logging level for log records to be looked at for deduplication purposes, defaults to ERROR
 *   - [time]: The period (in seconds) during which duplicate entries should be suppressed after a given log is sent through, defaults to 60
 *   - [bubble]: bool, defaults to true
 *
 * - group:
 *   - members: the wrapped handlers by name
 *   - [bubble]: bool, defaults to true
 *
 * - whatfailuregroup:
 *   - members: the wrapped handlers by name
 *   - [bubble]: bool, defaults to true
 *
 * - syslog:
 *   - ident: string
 *   - [facility]: defaults to 'user', use any of the LOG_* facility constant but without LOG_ prefix, e.g. user for LOG_USER
 *   - [logopts]: defaults to LOG_PID
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - syslogudp:
 *   - host: syslogd host name
 *   - [port]: defaults to 514
 *   - [facility]: defaults to 'user', use any of the LOG_* facility constant but without LOG_ prefix, e.g. user for LOG_USER
 *   - [logopts]: defaults to LOG_PID
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [ident]: string, defaults to
 *
 * - swift_mailer:
 *   - from_email: optional if email_prototype is given
 *   - to_email: optional if email_prototype is given
 *   - subject: optional if email_prototype is given
 *   - [email_prototype]: service id of a message, defaults to a default message with the three fields above
 *   - [content_type]: optional if email_prototype is given, defaults to text/plain
 *   - [mailer]: mailer service, defaults to mailer
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [lazy]: use service lazy loading, bool, defaults to true
 *
 * - native_mailer:
 *   - from_email: string
 *   - to_email: string
 *   - subject: string
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [headers]: optional array containing additional headers: ['Foo: Bar', '...']
 *
 * - symfony_mailer:
 *   - from_email: optional if email_prototype is given
 *   - to_email: optional if email_prototype is given
 *   - subject: optional if email_prototype is given
 *   - [email_prototype]: service id of a message, defaults to a default message with the three fields above
 *   - [mailer]: mailer service id, defaults to mailer.mailer
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - socket:
 *   - connection_string: string
 *   - [timeout]: float
 *   - [connection_timeout]: float
 *   - [persistent]: bool
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - pushover:
 *   - token: pushover api token
 *   - user: user id or array of ids
 *   - [title]: optional title for messages, defaults to the server hostname
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [timeout]: float
 *   - [connection_timeout]: float
 *
 * - raven / sentry:
 *   - dsn: connection string
 *   - client_id: Raven client custom service id (optional)
 *   - [release]: release number of the application that will be attached to logs, defaults to null
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [auto_log_stacks]: bool, defaults to false
 *   - [environment]: string, default to null (no env specified)
 *
 * - sentry:
 *   - hub_id: Sentry hub custom service id (optional)
 *
 * - newrelic:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [app_name]: new relic app name, default null
 *
 * - hipchat:
 *   - token: hipchat api token
 *   - room: room id or name
 *   - [notify]: defaults to false
 *   - [nickname]: defaults to Monolog
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [use_ssl]: bool, defaults to true
 *   - [message_format]: text or html, defaults to text
 *   - [host]: defaults to "api.hipchat.com"
 *   - [api_version]: defaults to "v1"
 *   - [timeout]: float
 *   - [connection_timeout]: float
 *
 * - slack:
 *   - token: slack api token
 *   - channel: channel name (with starting #)
 *   - [bot_name]: defaults to Monolog
 *   - [icon_emoji]: defaults to null
 *   - [use_attachment]: bool, defaults to true
 *   - [use_short_attachment]: bool, defaults to false
 *   - [include_extra]: bool, defaults to false
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [timeout]: float
 *   - [connection_timeout]: float
 *
 * - slackwebhook:
 *   - webhook_url: slack webhook URL
 *   - channel: channel name (with starting #)
 *   - [bot_name]: defaults to Monolog
 *   - [icon_emoji]: defaults to null
 *   - [use_attachment]: bool, defaults to true
 *   - [use_short_attachment]: bool, defaults to false
 *   - [include_extra]: bool, defaults to false
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - slackbot:
 *   - team: slack team slug
 *   - token: slackbot token
 *   - channel: channel name (with starting #)
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - cube:
 *   - url: http/udp url to the cube server
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - amqp:
 *   - exchange: service id of an AMQPExchange
 *   - [exchange_name]: string, defaults to log
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - error_log:
 *   - [message_type]: int 0 or 4, defaults to 0
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - null:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - test:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - debug:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - loggly:
 *   - token: loggly api token
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [tags]: tag names
 *
 * - logentries:
 *   - token: logentries api token
 *   - [use_ssl]: whether or not SSL encryption should be used, defaults to true
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [timeout]: float
 *   - [connection_timeout]: float
 *
 * - insightops:
 *   - token: Log token supplied by InsightOps
 *   - region: Region where InsightOps account is hosted. Could be 'us' or 'eu'. Defaults to 'us'
 *   - [use_ssl]: whether or not SSL encryption should be used, defaults to true
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - flowdock:
 *   - token: flowdock api token
 *   - source: human readable identifier of the application
 *   - from_email: email address of the message sender
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - rollbar:
 *   - id: RollbarNotifier service (mandatory if token is not provided)
 *   - token: rollbar api token (skip if you provide a RollbarNotifier service id)
 *   - [config]: config values from https://github.com/rollbar/rollbar-php#configuration-reference
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - server_log:
 *   - host: server log host. ex: 127.0.0.1:9911
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * All handlers can also be marked with `nested: true` to make sure they are never added explicitly to the stack
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 * @author Christophe Coevoet <stof@notk.org>

That is a lot. Some are obvious, others you will need to google and figure out.

You can get as wild and creative as you want though.

Config location is everything

Where you place your configuration is very important. A little more hint here.

If you want to log just during Development you put the configuration inside the dev folder. /app/config/packages/dev/monolog.yaml

If you want to log only during production you put the configuration in the prod folder. /app/config/packages/prod/monolog.yaml

If you want to log in all environments you place the configuration in the main configuration folder. /app/config/packages/monolog.yaml

So if you want a specific logger that logs to a specific file to be available in all environments define it in the “packages” folder.

It isn’t too bad once you figure it out.

Here is the official documentation on how configuration works. Read it if you need more information.

An example configuration

Before getting too much further lets look at the default file I have for the configuration of monolog in the development environment.

#file name -> /config/packages/dev/monolog.yaml
monolog:
    handlers:
        main:
            type: rotating_file
            max_files: 3
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            #path: php://stderr
            level: debug
            channels: ["!event"]
        # uncomment to get logging in your browser
        # you may have to allow bigger header sizes in your Web server configuration
        #firephp:
        #    type: firephp
        #    level: info
        #chromephp:
        #    type: chromephp
        #    level: info
        console:
            type: console
            process_psr_3_messages: false
            channels: ["!event", "!doctrine", "!console"]

Yours may differ, I have probably made changes to this and forgotten at this point or some changes in the Symfony code base may have happened.

After the word handlers you list each handler one tab level in.  Each handler gets it’s own configuration settings. Basically you pick a handler from the long list above, then configure it properly for your needs.

For local development I prefer the rotating_file option. Since it seems like it is the only one that you can put a limit on the number of files.

The stream option just infinitely fills a single file forever and ever. Unless you have some sort of log rotator as mentioned in the documentation.

If you have the time and urge you could even store the messages in a database then create a user interface in your admin panel to view the messages then delete them. Otherwise you will need some way of limiting the logs and log sizes.

The documentation refers to this list as a stack of handlers. You can have as many as you want, like I mentioned above.

Here is the default production configuration taken from here in the Symfony docs.

# file name -> /config/packages/prod/monolog.yaml
monolog:
    handlers:
        filter_for_errors:
            type: fingers_crossed
            # if *one* log is error or higher, pass *all* to file_log
            action_level: error
            handler: file_log

        # now passed *all* logs, but only if one log is error or higher
        file_log:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"

        # still passed *all* logs, and still only logs error or higher
        syslog_handler:
            type: syslog
            level: error

The Symfony docs say the following about production logs.

In the prod environment, logs are written to STDERR PHP stream, which works best in modern containerized applications deployed to servers without disk write permissions.

I’ll probably remove the line that logs locally to a file the file_log line. No need to infinitely fill a log I don’t read. You will need to decide what handlers you want in production and configure them.

Handler names

As you can see in the development configuration file above, there is a handler named “main”. You can give your handlers any name you want, but you need to tell Symfony what type it is with the type directive like you see in main.

main: 
  type: rotating_file 
  max_files: 3 
  path: "%kernel.logs_dir%/%kernel.environment%.log" 
  level: debug 
  channels: ["!event"]

The word “main” can be anything you desire, but notice how you must use type, then use one of the above handler names, then provide the options. Read more about handler names here in the Symfony docs. Basically you can call the handler anything even “asdfg” as long as you use the word “type” and list the type and other required information. More on this below.

Bubble?

That is the question!

One property that you will see on all of the types above is “bubble” Further digging into the github monolog documentation reveals the following about it.

Handlers also have a $bubble property which defines whether they block the record or not if they handled it. In this example, setting the MailHandler‘s $bubble argument to false means that records handled by the MailHandler will not propagate to the StreamHandler anymore.

Basically this means you need to define your loggers in the proper order if you want to have the handling stop at a particular logger for a particular situation. Otherwise each logger in your list does something each time and you might not want that. Like do you want errors to go to a log and also to the console during development?

One thing you may want to think about is where do you want your errors, exceptions and other logged data to be logged? This answer will be different for production vs development for sure. You may want to log just exceptions to a database in production, so that you can query it and show them in an admin user interface for example.

This part of the documentation speaks about logging to different locations.

Channels aka logging to multiple places

What are these channels?

The documentation speaks of channels. What are these channels? In the code above for the monolog.yaml development configuration you will see this line

 channels: ["!event", "!doctrine", "!console"]

Here in the documentation it says the following about channels

The Symfony Framework organizes log messages into channels. By default, there are several channels, including doctrine, event, security, request and more. The channel is printed in the log message and can also be used to direct different channels to different places/files.

Aha. So that is what those are. I wonder what the ! means? Does that mean it ignores those or does it mean it accepts those? Yes it does. Basically the above line would log everything except (event, doctrine and console.) Below you can see a list of all the channels.

So to clarify if you want a handler to log a message for a specific channel you can use the following syntax in the handler configuration.

channels: [security, doctrine]

If you want the handler to ignore a channel then you use this syntax in the handler configuration

channels: ["!event", "!doctrine", "!console"]

You can really get as insanely complex as you want with this logging stuff.

By the way how do you know what logging channels are available?

I prefer the following command, because these can be auto-wired :

php bin/console debug:autowiring

Scroll down and look for monolog listings. You will see something like this.(These can all be auto-wired)

console bin output

These are the channels you have.Basically whenever one of these experiences an issue Symfony sends out an event and the handlers you list decide if they want to do something about it. This way using the channels and multiple handlers you can log things exactly where you want them.  You can even log the same event/message multiple times in multiple locations.

As the docs mention, by default Symfony logs all events to the same location. It is up to you to change that to your projects needs.

Another useful command is the following which allows you to get detailed info about each, it also lists more, but not all are auto-wireable.

php bin/console debug:container monolog

That is interactive and you can choose one of the listings to get more information about it. I find the first method useful to find what I actually can access.

Creating Custom Channels

Sometimes you need to log a specific issue to a specific file or location. For this you need to create your own Channel. The documentation is really weak on this subject so lets dig deep into it.

I get tired of the foo bar BS so here I create a custom channel named “chicken” just so it sticks out for this example.

To create the channel you simple list it at the top of the monolog.yaml file like this.

monolog:
  channels: ['chicken']
  handlers:
    main:
      type: rotating_file
      max_files: 3
      path: "%kernel.logs_dir%/%kernel.environment%.log"
      #path: php://stderr
      level: debug
      channels: [ "!event" ]

A little confusing how you use that syntax in more than one location. In one it listens for channels in another it creates channels.

Wait what?

Maybe channel_list or something else to differentiate would have been better?

Placing the directive above the rest of the handler code creates the new channel or channels.  I don’t know if this is a requirement, but I know it works. To create more than one channel create strings separated by commas.

Now to check if anything happened I will use this command


php bin/console debug:autowiring

Now we have a chicken logger.

Now you can see “monolog.logger.chicken” logger that was created with that one line in the monolog.yaml configuration file.

That is how it is done.

How to use the logger?

There are two ways to use the logger, one you didn’t have to do any of this to use, the other is autowiring, which is why we did all of this for. The other way is to just initiate it and pass the values to the constructor like the documentation for Monolog shows.

Even though all that configuration seems like a pain, in the end it makes things easier by allowing us to auto-wire the logger we want. This saves a lot of having to create the same boilerplate code over and over all throughout our app.

Taking another look at the output of the bin/console debug:container command from above we can see the following

Lets now use the chickenLogger

Each line after the first monolog.logger contains what looks like a PHP variable. It is in fact a PHP variable, one you can use for type hinting. How this works is you type hint LoggerInterface, then use which variable you want and Symfony automagically loads it for you.

farth too easy meme

It feels too easy to be true

Back to the chickenLoggerExample. What I want to do with the chickenLogger is use it to log exceptions to their own exceptions log for easier viewing. This is useful in a situation like using a library to create and convert images.

class UserImageProcessor
{

    private LoggerInterface $chickenLogger;

    public function __construct(LoggerInterface $chickenLogger){
        $this->chickenLogger = $chickenLogger;
        $this->chickenLogger->debug('lets log some chickens');
    }

See in the constructor how I passed the variable $chickenLogger and now Symfony passes me the chickenLogger Channel and I can now log chickens. Logging chickens is a very handy feature in the real world.

And below is the whole monolog.yaml file I am using to log my chickens.

monolog:
  channels: ['chicken']
  handlers:
    main:
      type: rotating_file
      max_files: 3
      path: "%kernel.logs_dir%/%kernel.environment%.log"
      #path: php://stderr
      level: debug
      channels: [ "!event", "!chicken" ]

    exceptions:
      type: rotating_file
      max_files: 2
      path: "%kernel.logs_dir%/exceptions.log"
      file_permission: '765'
      level: debug
      channels: ['chicken']


    # uncomment to get logging in your browser
    # you may have to allow bigger header sizes in your Web server configuration
    #firephp:
    #    type: firephp
    #    level: info
    #chromephp:
    #    type: chromephp
    #    level: info
    console:
      type: console
      process_psr_3_messages: false
      channels: [ "!event", "!doctrine", "!console" ]

Notice I used the file permissions option. That is a handy one. See the exceptions handler is using the chicken channel. Again I could have named the “exceptions:” section anything even “chirpy-birds” symfony doesn’t care, only the directives matter.

Now when I log my chickens, the main handler section will ignore the chickens, but my exceptions section will log my chickens and so will the console. I’d have to add “!chicken” to the consoles “channels” section or I can add “bubbles: false” to the exceptions section to stop it.

Now you know!!!

Links about Loggers

Logging to Rollbar -> Helps you organize and view logs and much more.

Logging with Monolog -> A short article

Logging -> Documentation link ->overall general information

How to log messages to different files -> more from the documentations

Using the logger -> more from the docs on how to use it.

Symfony2 Console component, by example
09/04/2014

Deprecated: This article has been re-written — see
The Ultimate Developer Guide to Symfony — Console

TL;DR: jump to the conclusion.

Symfony2 is a set of libraries which help you in your
everyday tasks. You can even stack them together and create a framework with it:

  • Symfony standard edition
  • Symfony empty edition
  • Silex

Many frameworks already use a lot of components from Symfony2:

  • Laravel
  • Drupal
  • eZ Publish
  • PHPUnit
  • phpBB
  • Composer

In this article, we’ll see the
Console Component,
which allows you to build Command Line Interface (CLI) applications. Symfony 2.5
will be released in may 2014, with great new features for the Console, so I’ll
speak about this version here.

  • Introduction
    • Application
    • Command
    • Input
    • Output
    • ConsoleLogger
  • Standalone example
    • Creating the application
    • Creating the command
    • Registering the command
    • Using the Filesystem component
    • Thin controller, many small services
    • Registering the services

Introduction

This component allows you to focus on one thing: creating commands. It takes
care of all the coloring output, input gathering and command containing stuff.

The big picture is: you have an Application which contains a set of
Commands. When ran, the Application will create an Input object which
contains Options and Arguments provided by the user, and will feed it to
the right Command.

The code being the best documentation, we’ll now see the strict minimum classes
you should know, with the methods you’ll likely use.

Application

All you need to know about the Application is this:

<?php

namespace SymfonyComponentConsole;

use SymfonyComponentConsoleCommandCommand;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleOutputOutputInterface;

class Application
{
    public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN');
    public function add(Command $command);
    public function setDefaultCommand($commandName); // New in 2.5!
    public function run(InputInterface $input = null, OutputInterface $output = null);
}

Minimum usage:

#!/usr/bin/env php
<?php

use SymfonyComponentConsoleApplication;

$application = new Application();
$application->run();

By running this script, you should be able to see a colorful output which lists
the available commands (list is the default command, and a help is also
available).

Command

The Command class is the controller of your CLI application:

<?php

namespace SymfonyComponentConsoleCommand;

use SymfonyComponentConsoleInputInputArgument;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleInputInputOption;
use SymfonyComponentConsoleOutputOutputInterface;

class Command
{
    protected function configure();
    protected function execute(InputInterface $input, OutputInterface $output);
    protected function interact(InputInterface $input, OutputInterface $output);

    // To be called in configure
    public function setName($name);
    public function addArgument($name, $mode = null, $description = '', $default = null);
    public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null);
    public function setDescription($description);
    public function setHelp($help);
    public function setAliases($aliases);
}

Basically you create a class which extends Command. You need to implement 2
methods:

  • configure: the configuration of the command’s name, arguments, options, etc
  • execute: where you process the input, call your services and write to the
    output

The interact method is called before the execute one: it allows you to ask
questions to the user to set more input arguments and options.

Here’s my stand on arguments and options modes:

  • an argument should always be required (InputArgument::REQUIRED)
  • a flag is an option without value (InputOption::VALUE_NONE)
  • an option should always have a required value (InputOption::VALUE_REQUIRED),
    don’t forget to provide a default one

Input

The container of the arguments and options given by the user:

<?php

namespace SymfonyComponentConsoleInput;

interface InputInterface
{
    public function getArgument($name);
    public function getOption($name);
}

The Application validates a part of the input: it checks if the command
actually accepts the given arguments and options (is the value required? Does
the hello:world command have a --yell option? etc), but you still need to
validate the input against your business rules (the --number option should
be an integer, the name argument should be escaped to avoid SQL injection,
etc).

Output

A convenient object which allows you to write on the console output:

<?php

namespace SymfonyComponentConsoleOutput;

abstract class Output implements OutputInterface
{
    public function writeln($messages, $type = self::OUTPUT_NORMAL);
}

The writeln method allows you to write a new line (with a newline character at
the end). If the given message is an array, it will print each elements on a
new line.

The tags allow you to color some parts:

  • green text for informative messages (usage example: <info>foo</info>)
  • yellow text for comments (usage example: <comment>foo</comment>)
  • black text on a cyan background for questions (usage example: <question>foo</question>)
  • white text on a red background for errors (usage example: <error>foo</error>)

ConsoleLogger

Another brand new class from the version 2.5:

<?php

namespace SymfonyComponentConsoleLogger;

use PsrLogAbstractLogger;
use SymfonyComponentConsoleOutputOutputInterface;

class ConsoleLogger extends AbstractLogger
{
    public function __construct(
        OutputInterface $output,
        array $verbosityLevelMap = array(),
        array $formatLevelMap = array()
    );

    public function log($level, $message, array $context = array());
}

As you can see, it uses the OutputInterface provided by the Application.
You should inject this logger into your services, this will allow them to write
messages on the standard output of the console while keeping them decoupled from
this component (so you can use these services in a web environment).

Oh, and the good news is: it colors the output and decides whether or not to
print it depending on the verbosity and level of log! An error message would
always be printed in red, an informative message would be printed in green if
you pass the -vv option.

Standalone example

Just like any other component, the Console can be used as a standalone library.

In this example, we’ll create a tool which will create a LICENSE file, just
like fossil (the bootstraper of markdown files for your FOSS projetcs).

Creating the application

To begin, let’s install the component using Composer:

$ curl -sS https://getcomposer.org/installer | php # Downloading composer
$ ./composer.phar require "symfony/console:~2.5@dev"

Then create an empty application:

#!/usr/bin/env php
<?php
// File: fossil

require __DIR__.'/vendor/autoload.php';

use SymfonyComponentConsoleApplication;

$application = new Application('Fossil', '2.0.0');
$application->run();

Creating the command

Our command has two arguments:

  • the name for the copyright
  • the year for the copyright

It can also take the path of the project as an option (we’ll provide the
current directory as default value).

Let’s create it:

<?php
// File: src/Gnugat/Fossil/LicenseCommand.php

namespace GnugatFossil;

use SymfonyComponentConsoleCommandCommand;
use SymfonyComponentConsoleInputInputArgument;
use SymfonyComponentConsoleInputInputOption;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleOutputOutputInterface;

class LicenseCommand extends Command
{
    protected function configure()
    {
        $this->setName('license');
        $this->setDescription('Bootstraps the license file of your project');

        $this->addArgument('author', InputArgument::REQUIRED);
        $this->addArgument('year', InputArgument::REQUIRED);

        $this->addOption('path', 'p', InputOption::VALUE_REQUIRED, '', getcwd());
    }

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

Registering the command

Our command doesn’t do anything yet, but we can already register it in our
application:

#!/usr/bin/env php
<?php
// File: fossil

require __DIR__.'/vendor/autoload.php';

use SymfonyComponentConsoleApplication;
use GnugatFossilLicenseCommand;

$command = new LicenseCommand();

$application = new Application('Fossil', '2.0.0');
$application->add($command);
$application->run();

In order for it to run, you’ll need to register the namespace in the autoloader
by editing the composer.json file at the root of the project:

{
    "require": {
        "symfony/console": "~2.5@dev"
    },
    "autoload": {
        "psr-4": { "": "src" }
    }
}

Then you need to run ./composer.phar update to update the configuration.

Using the Filesystem component

In fossil, templates
are retrieved using the
Finder component, their
values are replaced using Twig and written using the
Filesystem component.

In order to keep this article short, we’ll:

  • use a fictive license which requires only the copyright line
  • simply store the LICENSE template in the command
  • inject the values using implode

This means that you have to install the new component:

$ ./composer.phar require "symfony/filesystem:~2.4"

And then you need to fill the execute method:

<?php
// File: src/Gnugat/Fossil/LicenseCommand.php

namespace GnugatFossil;

use SymfonyComponentConsoleCommandCommand;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleOutputOutputInterface;
use SymfonyComponentFilesystemFilesystem;

class LicenseCommand extends Command
{
    // configure method...

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $path = $input->getOption('path').'/LICENSE';
        $license = implode(' ', array(
            'Copyright (c)',
            $input->getArgument('author'),
            $input->getArgument('year'),
        ));

        $filesystem = new Filesystem();
        $filesystem->dumpFile($path, $license.PHP_EOL);

        $output->writeln(sprintf('Created the file %s', $path));
    }
}

Now running ./fossil license "Loïc Chardonnet" "2013-2014" -p="/tmp" will
output the message «Created the file /tmp/LICENSE», which should be what really
happened.

Thin controller, many small services

I’m not a big fan of putting logic in my commands, so generally I use services
to do the actual job:

<?php
// File src/Gnugat/Fossil/DocumentationWriter.php

namespace GnugatFossil;

use SymfonyComponentFilesystemFilesystem;
use PsrLogLoggerInterface;

class DocumentationWriter
{
    private $filesystem;
    private $logger;

    public function __construct(Filesystem $filesystem, LoggerInterface $logger)
    {
        $this->filesystem = $filesystem;
        $this->logger = $logger;
    }

    public function write($path, $content)
    {
        $this->filesystem->dumpFile($path, $content);
        $this->logger->notice(sprintf('Created file %s', $path));
    }
}

As you can see, the DocumentationWriter isn’t very big. It might seem
overkill, but now it’s easy to write tests which will check if the LICENSE
file has been created. Also, in fossil the class does a bit more work: it
checks if the file already exists, and takes a «force overwrite» option into
account.

You’ll also notice that we inject a logger to notice the user of what happens.
We need to install the PSR-3 logger interface:

$ composer require "psr/log:~1.0"

Our command will now be much thinner, just like any controller should be (MVC
can also be applied in CLI):

<?php
// File: src/Gnugat/Fossil/LicenseCommand.php

namespace GnugatFossil;

use GnugatFossilDocumentationWriter;
use SymfonyComponentConsoleCommandCommand;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleLoggerConsoleLogger;
use SymfonyComponentConsoleOutputOutputInterface;
use SymfonyComponentFilesystemFilesystem;

class LicenseCommand extends Command
{
    // configure method...

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $path = $input->getOption('path').'/LICENSE';
        $license = implode(' ', array(
            'Copyright (c)',
            $input->getArgument('author'),
            $input->getArgument('year'),
        ));

        $filesystem = new Filesystem();
        $logger = new ConsoleLogger($output);
        $documentationWriter = new DocumentationWriter($filesystem, $logger);

        $documentationWriter->write($path, $license.PHP_EOL);
    }
}

To be fair, our command is longer. But it is thinner as it now has less
responsibilities:

  • it retrieves the input
  • creates the dependencies
  • calls the services

If you run again ./fossil license "Loïc Chardonnet" "2013-2014" -p="/tmp",
you won’t see anything: ConsoleLogger hides informative messages by default.
You need to pass the verbose option to see the message:

$ ./fossil license -v "Loïc Chardonnet" "2013-2014" -p="/tmp"

Registering the services

The dependency creation isn’t a responsibility a controller should have. We’ll
delegate this to the
Dependency Injection component:

$ ./composer.phar require "symfony/dependency-injection:~2.4"

We’ll also install the
Config component:

$ ./composer.phar require "symfony/config:~2.4"

If you don’t know yet this component, go read
this helpful article.

We’ll create a XML file to configure the registration of our services:

<?xml version="1.0" ?>

<!-- File: config/services.xml -->

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <services>
        <service id="symfony.application"
            class="SymfonyComponentConsoleApplication">
            <argument key="name">Fossil</argument>
            <argument key="version">2.0.0</argument>
            <call method="add">
                 <argument type="service" id="fossil.license_command" />
            </call>
        </service>

        <service id="fossil.license_command" class="GnugatFossilLicenseCommand">
            <argument type="service" id="fossil.documentation_writer" />
        </service>

        <service id="fossil.documentation_writer" class="GnugatFossilDocumentationWriter">
            <argument type="service" id="symfony.filesystem" />
            <argument type="service" id="symfony.console_logger" />
        </service>

        <service id="symfony.filesystem" class="SymfonyComponentFilesystemFilesystem">
        </service>

        <service id="symfony.console_logger" class="SymfonyComponentConsoleLoggerConsoleLogger">
            <argument type="service" id="symfony.console_output" />
        </service>

        <service id="symfony.console_output"
            class="SymfonyComponentConsoleOutputConsoleOutput">
        </service>
    </services>
</container>

As you can see, I’ve delegated every construction to the DIC (Dependency
Injection Container), even the construction of the application. Now the command
looks like this:

    <?php
// File: src/Gnugat/Fossil/LicenseCommand.php

namespace GnugatFossil;

use GnugatFossilDocumentationWriter;
use SymfonyComponentConsoleCommandCommand;
use SymfonyComponentConsoleInputInputArgument;
use SymfonyComponentConsoleInputInputOption;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleOutputOutputInterface;

class LicenseCommand extends Command
{
    private $documentationWriter;

    public function __construct(DocumentationWriter $documentationWriter)
    {
        $this->documentationWriter = $documentationWriter;

        parent::__construct();
    }

    protected function configure()
    {
        $this->setName('license');
        $this->setDescription('Bootstraps the license file of your project');

        $this->addArgument('author', InputArgument::REQUIRED);
        $this->addArgument('year', InputArgument::REQUIRED);

        $this->addOption('path', 'p', InputOption::VALUE_REQUIRED, '', getcwd());
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $path = $input->getOption('path').'/LICENSE';
        $license = implode(' ', array(
            'Copyright (c)',
            $input->getArgument('author'),
            $input->getArgument('year'),
        ));

        $this->documentationWriter->write($path, $license.PHP_EOL);
    }
}

And the console now contains the DIC initialization:

#!/usr/bin/env php
<?php
// File: fossil

use SymfonyComponentConfigFileLocator;
use SymfonyComponentDependencyInjectionContainerBuilder;
use SymfonyComponentDependencyInjectionLoaderXmlFileLoader;

require __DIR__.'/vendor/autoload.php';

$container = new ContainerBuilder();
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/config'));
$loader->load('services.xml');

$output = $container->get('symfony.console_output');

$application = $container->get('symfony.application');
$application->run(null, $output);

And voilà! You now know how to create CLI applications :) .

Conclusion

The Console component allows you to create CLI applications. The commands are a
thin layer which gathers the input and call services. Those services can then
output messages to the user using a special kind of logger.

Although this article was a bit long, I might have missed something here, so
if you have any feedbacks/questions, be sure to contact me on
Twitter.

Понравилась статья? Поделить с друзьями:
  • Symfony 500 internal server error
  • Symfony 404 error
  • Symbols error huawei что это
  • Symbol lookup error undefined symbol linux
  • Symbol error rate это