Building a Console Application with Symfony
When I first started to learn how to program, I was limited only to boring simple console based programs. Now, I have the ability to create wonderful (and sometimes horrific) GUIs.
But console based applications or tools still have their place. But thy don't have to be boring and definitely do not have to be simple.
In this tutorial, I will show you how to utilise Symfony's Console Component by building a simple trivia game.
You can find the full working code that I used in this post on my GitHub.
Dependencies on Composer
As usual, the easiest way to get the dependencies that we need is with composer. If you need a little help getting started with this, I wrote a brief guide some years back which can check out.
My composer.json
has only 3 items:
{
"require": {
"symfony/console": "3.2.*",
"symfony/yaml": "3.2.*",
"symfony/class-loader": "3.2.*"
}
}
The Console Component, which is what this is all about. The Yaml Component which I will use for reading the questions file (optional, if you store the questions as JSON, for example, you can just use native PHP). And the Class Loader Component which I use for autoloading my classes (also optional).
Yaml Component
YAML stand for "YAML Ain't a Markup Language" and is described by its spec as a "straightforward machine parsable data serialization format designed for human readability".
It is commonly used for configuration files and if you are not using already, I highly recommend taking a look. It is far better than XML and I think is a little more readable than JSON.
PHP does not currently have a native way for parsing YAML (like it does with XML and JSON), but Symfony's Yaml Component does the job perfectly.
So my YAML file looks a little like this:
questions:
-
question: Which Apollo mission landed the first humans on the Moon?
wrong_answers:
- Apollo 7
- Apollo 9
- Apollo 13
answer: Apollo 11
-
question: What is the International Air Transport Association airport code for Heathrow Airport?
wrong_answers:
- HRW
- HTR
- LHW
answer: LHR
As you can see, I have marked out the question, the wrong answers and the correct answer (as it is going to be multiple choice). I am not going to go into the whole YAML syntax, but the -
(and the indentations) signify a numeric array. This will basically translate into a multi dimensional array of arrays.
In my code, I have a class that will hold each question (called Question) and another class called QuestionFactory
that will parse the file and generate an array of Question
objects.
To read the file, first I include the Yaml library for use:
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Yaml\Exception\ParseException;
try {
$ymlData = Yaml::parse(file_get_contents($questionFilePath));
} catch (ParseException $e) {
printf("Unable to parse the YAML string: %s", $e->getMessage());
}
It is that simple. $ymlData
now holds the array that is formed from parsing the file. This is why I love this component so much.
I can now loop through each item of the array and create my objects.
I am not going to go into detail, you can look at the code yourself, but the Question
constructor randomly assigns each wrong answer to A, B, C or D and the right answer to the remaining letter. Each of these are variables of the class.
It also sets which letter was the chosen one for the correct answer.
This way, each time you play, the order of the answers are random as are the questions themselves.
Console Component
Now for the cool stuff.
So first, I have a normal index file and I have these lines of code:
use Symfony\Component\Console\Application;
use TriviaGame\GameCommand;
use TriviaGame\QuestionFactory;
$questions = QuestionFactory::generateQuestion('questions.yml');
$trivia_app = new Application();
$trivia_app->add(new GameCommand($questions));
$trivia_app->run();
As I said, $questions
is just an array of objects with the information loaded from the YAML file.
To make the console application, we make a new Application
, add the commands and then run it. Done. Very very simple.
Obviously the meat of the command is inside GameCommand
itself.
Every command that you make, needs to inherit from Symfony\Component\Console\Command\Command
. If you override the constructor as I did, you need to remember to call the parent constructor when you have finished running your set up code:
parent::__construct(null);
There are two functions that every command needs to implement:
protected function configure()
which is run during the construction stage in the parent. And
protected function execute(InputInterface $input, OutputInterface $output)
which is the actual code that is run when calling this command.
Basic Output
The execute
function has two inputs:
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
As can probably guess, these are for managing inputs and outputs for the console. We will look more at the output.
To write something to to the console, simple use writeln
:
$output->writeln('Hello, '.$name.'!');
You could use write
, but writeln
deals with line breaks automatically. Also, you can pass an array which will automatically be broken down to new lines.
Types of Questions
Now the confusing part here is that I am building a trivia game that has questions but also the console application has "questions" itself, which is how it gets inputs from the user once the execution has begun.
There are several types of questions provided by this component and this trivia app makes use of 3 types. I won't go into the ins and outs of how the questions are used (you can see that for yourself in the code), but here are the relevant snippets of code.
Normal Question
First off, we have what I call the "Normal Question": there is a prompt for information, and the user types whatever they like.
First we include the class we need:
use Symfony\Component\Console\Question\Question;
Inside the execute
function, I get the class that asks the question:
$questionMaster = $this->getHelper('question');
I will use this questionMaster
in the remaining examples, but you only need it once.
Now for the question. Start with a creation of a Question object. The parameter is the text that it will be. Be sure to add a trailing space if you are fussy about this kind of thing:
$question = new Question('Please enter your name: ');
Then use the $questionMaster
to ask:
$name = $questionMaster->ask($input, $output, $question);
$name
will store whatever the user inputs. You do not have to worry about any implementation details (like waiting for them to press enter.
The code is sequential, so it "pauses" execution to ask the question and once they have answered, it will move onto the next line with the answer stored.
You can choose at this point to validate the response and act accordingly. For example, if they leave it blank and just press enter, I will not proceed until I have an answer:
$name = '';
while (empty($name)) {
$name = $questionMaster->ask($input, $output, $question);
}
Confirmation Question
It is true that every type of question can, in theory, be implemented using the generic Question
class described above.
But Symfony have helpfully done the work for us with several implementations.
This one is the "Confirmation Question":
Symfony\Component\Console\Question\ConfirmationQuestion;
As the name suggests, this is for when you want to ask a yes or no question. Looks like this:
$question = new ConfirmationQuestion('Are you ready to begin (type "yes" if you are)?', false, '/^(yes)/i');
The first parameter is the question text itself. The second parameters is the default value if the question is skipped (the user just presses Enter). The default value for this is true
. The final parameter is the is a regex for saying "yes". The default for this is "y", in this example, I require "yes".
This function returns true
if the user matches the regex in the third parameter, or false
otherwise. If they do not write anything, it will return the default value specified in the second parameter.
So if it is quite a serious question, you will obviously not want to make the default value true
. If it is very serious, you can make them type more letters other than just "y".
Just like before, you use the $questionMaster
created earlier to ask the question, and the response is stored in a variable.
$begin = $questionMaster->ask($input, $output, $question);
Then you can act accordingly (for example, exit the program if it is false).
Multiple Choice Question
For my multiple choice trivia questions, Symfony's Choice Question, was perfect.
use Symfony\Component\Console\Question\ChoiceQuestion;
So I create the question:
$getAnswer = new ChoiceQuestion(
['Question '.($questionNumber + 1), $question->question],
[
'A' => $question->option_A,
'B' => $question->option_B,
'C' => $question->option_C,
'D' => $question->option_D,
],
null);
The first parameter is, again, the question. We can actually provide an array and it will line break each item automatically (like with the writeln
earlier).
The second parameter is the array of options. By default, you can just provide it values which become assigned to numbered labels (starting from 0). But by doing an associative array, you can specify the labels.
The third parameter is the default value which in this case I have set to null
.
In my example, as you will see from looking at the code, I have this in a foreach
loop and I am just accessing the properties of my Question
class that I spoke about earlier.
As before, to get the answer, just ask
and store the response:
$answer = $questionMaster->ask($input, $output, $getAnswer);
Both the index (A, B, C or D) and the values themselves, will be automatically accepted as answers, but the output will always be the index.
Just to see how all this looks:
All the validation messages around the multiple choice questions is already all done for us which is excellent!
Styling
This is all great. We have a fully functional application for use in a console at minimal effort. We have been able to focus mostly on the logic of the application and leave the complexities of interfacing with the console up to Symfony.
But, it still looks a bit bland. Wouldn't it be nice to add a splash of colour? Luckily, Symfony has helped with that also:
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
With this, you can set up a style and use it in all the outputs.
First I make a new style:
$style = new OutputFormatterStyle('white', 'blue', []);
The first parameter is the text colour, second parameter the background colour and the third parameter is some extra options.
The available colours are: black, red, green, yellow, blue, magenta, cyan and white. Also the value, "default" is acceptable and just leaves the text as the console's default.
The options that you can provide are: bold, underscore, blink, reverse and conceal.
Once you have your style, you then attach it to output and assign it a label.
$output->getFormatter()->setStyle('dialog', $style);
To use it, just wrap the text in tags with the label:
$output->writeln('<dialog>Hello, '.$name.'!</dialog>');
Simple.
If you are using an array to print a multiline block of text, you can also surround these with tags by adding the opening and closing tags as array items:
$style = new OutputFormatterStyle('magenta', 'default', []);
$output->getFormatter()->setStyle('quiz_question', $style);
$getAnswer = new ChoiceQuestion(
['<quiz_question>', 'Question '.($questionNumber + 1), $question->question, '</quiz_question>'],
[
'A' => $question->option_A,
'B' => $question->option_B,
'C' => $question->option_C,
'D' => $question->option_D,
],
null);
Now if we look at the application:
Much nicer! Again with minimal effort.
One thing that I would add is how to add new lines in between.
First we use the SymfonyStyle class:
use Symfony\Component\Console\Style\SymfonyStyle;
Create the object passing the input and output objects given in the execute
function:
$io = new SymfonyStyle($input, $output);
Then, just call newLine()
whenever you want to add one:
$io->newLine();
You can also provide an integer to give a number of new lines.
Summary
So there you have it: how to make a console application using Symfony's Console Component. As usual, I have barely scratched the surface. It is capable of letting you do some powerful things.
You can read all about it in their excellent documentation .
Again, you can find the full working example here.