Support for command line applications.
stubbles/console is distributed as Composer package. To install it as a dependency of your package use the following command:
composer require "stubbles/console": "^7.0"
stubbles/console requires at least PHP 7.0.
The main point for command line applications are their Console app classes. They
must extend stubbles\console\ConsoleApp
, and have two purposes:
- Provide a list of binding modules for the Stubbles IoC mechanism that tells it how to wire the object graph for the whole application.
- Get the main entry of the logic injected and run it within its
run()
method. Depending on the outcome of the logic it should return a proper exit code, which means 0 for a successful execution and any other exit code for in case an error happened.
For details about the IoC part check the docs about Apps in stubbles/ioc. Generally, understanding the Inversion of Control functionality in stubbles/ioc will help a lot in regard on how to design the further classes in your command line app.
For an example of how a console app class might look like check
ConsoleAppCreator,
the Console app class behind the createConsoleApp
script explained below.
The Console app class' run()
method should return a proper exit code. It
should be 0 if the run was successful, and a non-zero exit code in case an error
occurred.
It is recommended to use exit code between 21 and 255 for errors. Exit codes 1 to 20 are reserved by Stubbles Console and should not be used, whereas the upper limit of 255 is a restriction by some operating systems.
- 10 Returned when one of the command line options or arguments for the app contains an error.
- 20 Returned when an uncatched exception occurred during the run of the console app class.
In order to ease creation of console apps stubbles/console provides a helper script that can create the skeleton of a Console app class, the fitting command line script to call this Console app, and a unit test skeleton for this console app class.
When in the root directory of your project and stubbles/console is installed, simply type
vendor/bin/createConsoleApp
First it will ask you for the full qualified name of the console app class to create. Type it into the prompt and confirm with enter. There is no need to escape the namespace separator. Second, it will ask you for the name of the command line script which should be created. Type it's name into the prompt and confirm with enter.
Once the script finishes you will find three new files in your application:
- a class in src/main/php with the class name you entered, extending the
stubbles\console\ConsoleApp
class. - a script in the bin folder with the name you typed in second.
- a unit test for the app class in src/test/php with the class name you entered.
At this point the app class, the script and the unit test are already fully functional - they can be run but of course will do nothing.
From this point you can start and extend the generated class and unit test with the functionality you want to implement.
What happens if the entered class already exists?
The creating script will check if a class with the given name already exists within the project. This includes all classes that can be loaded via the autoload functionality. If it does exist, creation of the app class is skipped.
What happens if the script to be created already exists?
Creation of the script will be skipped.
What happens if a unit test in src/test/php with this class name already exists?
Creation of the unit test will be skipped.
Can I use the createConsoleApp script to generate a script or a unit test for an already existing app?
Yes, this is possible. Just enter the name of the existing app class. As the class already exists, it's creation will be skipped, but the script and unit test will still be created if they don't exist yet.
When compiling the list of binding modules in the __bindings()
method of your
console app class, you can make use of binding modules provided by
stubbles/console. The stubbles\console\ConsoleApp
class which your own console
app class should extend provides static helper methods to create those binding
modules:
Creates a binding module that parses any arguments passed via command line and adds them as bindings to become available for injection. See below for more details about parsing command line arguments.
Most times a command line app needs arguments to be passed by the caller of the script. When the argument binding module is added to the list of binding modules (see above) stubbles/console will make those arguments available for injection into the app's classes.
By default all arguments given via command line are made available as an array
and as single values. Suppose the command line call was ./exampleScript foo bar baz
then the following values will be available for injection:
@Named('argv')
: the whole array of input values:array('foo', 'bar', 'baz')
.@Named('argv.0')
: the first value that is not the name of the called script, in this case foo.@Named('argv.1')
: the second value that is not the name of the called script, in this case bar.@Named('argv.2')
: the third value that is not the name of the called script, in this case baz.
Requesting a value that was not passed, e.g. @Named('argv.3')
in this example,
will result in a stubbles\ioc\BindingException
.
In some cases you need more sophisticated argument parsing. This is also possible:
self::argumentParser()
->withOptions($options)
->withLongOptions(array $options)
Using this we can parse arguments like this:
./exampleScript -n foo -d bar --other baz -f --verbose
To get a proper parsing for this example the arguments binding module must be configured as follows:
self::argumentParser()
->withOptions('fn:d:')
->withLongOptions(array('other:', 'verbose'))
For more details about the grammar for the options check PHP's manual on getopt().
If arguments are parsed like this they become available for injection with the following names:
@Named('argv')
: the whole array of input values:array('n' => 'foo', 'd'=> 'bar', 'other' => 'baz', 'f' => false, 'verbose' => false)
.@Named('argv.n')
: value of the option -n, in this case foo.@Named('argv.d')
: value of the option -d, in this case bar.@Named('argv.other')
: value of the option --other, in this case baz.@Named('argv.f')
: value of the option -f, in this casefalse
.@Named('argv.verbose')
: value of the option --verbose, in this casefalse
.
Any value being false
is due to the fact that PHP's getopt()
function sets
the values for arguments without values to false
.
Requesting a value that was not passed, e.g. @Named('argv.invalid')
in this
example, will result in a stubbles\ioc\BindingException
.
For each console app there should be a runner script that can be used to execute
the console app. When using the createConsoleApp
script (see above) such a
script will be created automatically.
Having all code required in an app in an app class has a huge advantage: you can create a unit test that makes sure that the whole application with all dependencies can be created. This means you can have a unit test like this:
/**
* @test
*/
public function canCreateInstance()
{
$this->assertInstanceOf(
MyConsoleApp::class,
MyConsoleApp::create(new Rootpath())
);
}
This test makes sure that all dependencies are bound and that an instance of the app can be created. If you also have unit tests for all the logic you created and you run those tests you can be pretty sure that the application will work.
The unit test created with createConsoleApp
will already provide two tests:
- A test that makes sure that the
run()
method returns with exit code 0 after a successful run. - And finally a test that makes sure that an instance of the app can be created (see above for how this looks like).
From this point on it should be fairly easy to extend this unit test with tests for the logic you implement in your app class.
In order to read user input from the command line one can use the
stubbles\console\ConsoleInputStream
. It is a normal input stream
from which can be read.
If you want to get such an input stream injected in your class it is recommended
to typehint against stubbles\streams\InputStream
and add a @Named('stdin')
annotation for this parameter:
/**
* receive input stream to read from command line
*
* @param InputStream $in
* @Named('stdin')
*/
public function __construct(InputStream $in)
{
$this->in = $in;
}
To write to the command line there are two possibilities: either write directly to standard out, or write to the error out. Both ways are implemented as an output stream.
If you want to get such an output stream injected in your class it is
recommended to typehint against stubbles\streams\OutputStream and add a
@Named('stout')
or @Named('sterr')
respectively annotation for these
parameters:
/**
* receive streams for standard and error out
*
* @param OutputStream $out
* @param OutputStream $err
* @Named{out}('stdout')
* @Named{err}('stderr')
*/
public function __construct(OutputStream $out, OutputStream $err)
{
$this->out = $out;
$this->err = $err;
}
Sometimes there are situations when you need to read from and to write to
command line at the same time. That's where stubbles\console\Console
comes
into play. It provides a facade to stdin input stream, stdout and stderr output
streams so you have a direct dependency to one class only instead of three. The
class provides methods to read and write:
Available since release 2.1.0.
Writes a message to stdout and returns a value reader similar to
reading request parameters.
In case you need access to error messages that may happen during value
validation you need to supply stubbles\input\ParamErrors
, errors will be
accumulated therein under the param name stdin.
Available since release 2.1.0.
Similar to prompt()
, but without a message
Available since release 2.1.0.
Asks the user to confirm something. Repeats the message until user enters y or
n (case insensitive). In case a default is given and the users enters nothing
this default will be used - if the default is y it will return true
, and
false
otherwise.
Reads input from stdin.
Reads input from stdin with line break stripped.
Write message to stdout, in case of an input stream the contents of that stream are copied to stdout.
Write a line to stdout.
Available since release 2.6.0. Write an empty line to stdout.
Write error message to stderr, in case of an input stream the contents of that stream are copied to stderr.
Write an error message line to stderr.
Available since release 2.6.0. Write an empty error message line to stderr.
From time to time it is necessary to run another command line program from within
your application. stubbles/console provides a convenient way to do this via the
stubbles\console\Executor
class.
It provides three different ways to run a command line program:
execute(string $command, callable $out = null)
: This will simply execute the given command. If the executor receives an callable the callable will be executed for each single line of the command's output.executeAsync(string $command): InputStream
: This will execute the command, but reading the output of the command can be done later via the returnedInputStream
.outputOf(string $command): \Generator
: This will execute the given command and return a Generator which yields each single line from the command's output as it occurs. (Available since release 6.0.0.)
If the executed command returns an exit code other than 0 this is considered as
failure, resulting in a \RuntimeException
.
If you want to redirect the output of the command to execute you can provide a redirect as an optional last argument for each of the methods listed above. By default the error output of a command is redirected to the standard output using 2>&1.
Running a command, discarding its output:
$executor->execute('git clone git://github.com/stubbles/stubbles-console.git');
Running a command and retrieve the output:
$executor->execute('git clone git://github.com/stubbles/stubbles-console.git', [$myOutputStream, 'writeLine']);
Running a command asynchronously:
$inputStream = $executor->executeAsync('git clone git://github.com/stubbles/stubbles-console.git');
// ... do some other work here ...
while (!$inputStream->eof()) {
echo $inputStream->readLine();
}
Directly receive command output:
foreach ($executor->outputOf('git clone git://github.com/stubbles/stubbles-console.git') as $line) {
echo $line;
}
Sometimes it's sufficient to collect the output of a command in a separate
variable. This can be done using the stubbles\console\collect()
function:
$out = '';
$executor->execute('git clone git://github.com/stubbles/stubbles-console.git', collect($out));
Afterwards, $out
contains all output from the command, separated by PHP_EOL
.
Alternatively, an array can be used, each element in the array will be a line
from the command output then.