Overview
CLI stands for Command Line Interface. CLI applications are executed from the command line or a shell prompt. One of the benefits of CLI applications is that they do not have a view layer (only potentially echoing output on the screen) and can be run more than once at a time. Some common usages are cron job tasks, manipulation scripts, import data scripts, command utilities, and more.
Structure
You can create a CLI application in Phalcon, using the Phalcon\Cli\Console class. This class extends from the main
abstract application class and uses a directory in which the Task scripts are located. Task scripts are classes that
extend Phalcon\Cli\Task and contain the code that needs to be executed.
The directory structure of a CLI application can look like this:
src/tasks/MainTask.php
php cli.phpIn the above example, the cli.php is the entry point of our application, while the src/tasks directory contains all
the task classes that handle each command.
Bootstrap
As seen above, the entry point of our CLI application is the cli.php. In that script, we need to bootstrap our
application with relevant services, directives, etc. This is similar to the all-familiar index.php that we use for MVC
applications.
<?php
declare(strict_types=1);
use Exception;
use Phalcon\Autoload\Loader;
use Phalcon\Cli\Console;
use Phalcon\Cli\Dispatcher;
use Phalcon\Cli\Console\Exception as PhalconException;
use Phalcon\Di\FactoryDefault\Cli as CliDI;
use Throwable;
$loader = new Loader();
$loader->setNamespaces(
[
'MyApp' => 'src/',
]
);
$loader->register();
$container = new CliDI();
$dispatcher = new Dispatcher();
$dispatcher->setDefaultNamespace('MyApp\Tasks');
$container->setShared('dispatcher', $dispatcher);
$container->setShared('config', function () {
return include 'app/config/config.php';
});
$console = new Console($container);
$arguments = [];
foreach ($argv as $k => $arg) {
if ($k === 1) {
$arguments['task'] = $arg;
} elseif ($k === 2) {
$arguments['action'] = $arg;
} elseif ($k >= 3) {
$arguments['params'][] = $arg;
}
}
try {
$console->handle($arguments);
} catch (PhalconException $e) {
fwrite(STDERR, $e->getMessage() . PHP_EOL);
exit(1);
} catch (Throwable $throwable) {
fwrite(STDERR, $throwable->getMessage() . PHP_EOL);
exit(1);
} catch (Exception $exception) {
fwrite(STDERR, $exception->getMessage() . PHP_EOL);
exit(1);
}Let’s look at the code above in more detail.
First, we need to create all the necessary services for our CLI application. We are going to create a loader to autoload our tasks, the CLI application, a dispatcher, and a CLI Console application. These are the minimum amount of services that we need to instantiate to create a CLI application.
Loader
$loader = new Loader();
$loader->setNamespaces(
[
'MyApp' => 'src/',
]
);
$loader->register();Create the Phalcon autoloader and register the namespace to point to the src/ directory.
DI
$container = new CliDI();We need a Dependency Injection container. You can use the Phalcon\Di\FactoryDefault\Cli container, which already has
services registered for you. Alternatively, you can always use the Phalcon\Di and register the services you need, one
after another.
Dispatcher
$dispatcher = new Dispatcher();
$dispatcher->setDefaultNamespace('MyApp\Tasks');
$container->setShared('dispatcher', $dispatcher);CLI applications need a specific dispatcher. Phalcon\Cli\Dispatcher offers the same functionality as the main
dispatcher for MVC applications, but it is tailored to CLI applications. As expected, we instantiate the dispatcher
object, set our default namespace, and then register it in the DI container.
Config
$container->setShared(
'config',
function () {
return include 'app/config/config.php';
}
);The above snippet is optional but will allow you to access any configuration settings you have set up.
Make sure to update the include path to be relative to where your cli.php file is.
Application
$console = new Console($container);As mentioned above, a CLI application is handled by the Phalcon\Cli\Console class. Here we instantiate it and pass it
to the DI container.
Arguments
Our application needs arguments. These come in the form of:
php ./cli.php argument1 argument2 argument3 ...The first argument relates to the task to be executed. The second is the action, and after that follow the parameters we need to pass.
$arguments = [];
foreach ($argv as $k => $arg) {
if ($k === 1) {
$arguments['task'] = $arg;
} elseif ($k === 2) {
$arguments['action'] = $arg;
} elseif ($k >= 3) {
$arguments['params'][] = $arg;
}
}As you can see in the above, we use the $argv to receive what has been passed through the command line, and we split
those arguments accordingly to understand what task and action need to be invoked and with what parameters.
So for the following example:
php ./cli.php users recalculate 10Our application will invoke the UsersTask, call the recalculate action and pass the parameter 10.
Execution
try {
$console->handle($arguments);
} catch (PhalconException $e) {
fwrite(STDERR, $e->getMessage() . PHP_EOL);
exit(1);
} catch (Throwable $throwable) {
fwrite(STDERR, $throwable->getMessage() . PHP_EOL);
exit(1);
} catch (Exception $exception) {
fwrite(STDERR, $exception->getMessage() . PHP_EOL);
exit(1);
}In the code above, we use our console object and call handle with the calculated parameters. The CLI application will
do the necessary routing and dispatch the task and action requested. If an exception is thrown, it will be caught by the
catch statements, and errors will be displayed on the screen accordingly.
Tasks
Tasks are the equivalent of controllers in an MVC application. Any CLI application needs at least one task called
MainTask and a mainAction. Any task defined needs to have a mainAction which will be called if no action is
defined. You are not restricted to the number of actions that each task can contain.
An example of a task class (src/Tasks/MainTask.php) is:
<?php
declare(strict_types=1);
namespace MyApp\Tasks;
use Phalcon\Cli\Task;
class MainTask extends Task
{
public function mainAction()
{
// This is the default task and the default action
echo '000000' . PHP_EOL;
}
}You can implement your tasks by either extending the supplied Phalcon\Cli\Task or writing your own class implementing
the Phalcon\Cli\TaskInterface.
Actions
As seen above, we have specified the second parameter to be the action. The task can contain more than one action.
<?php
declare(strict_types=1);
namespace MyApp\Tasks;
use Phalcon\Cli\Task;
class UsersTask extends Task
{
public function mainAction()
{
// This is the default task and the default action
echo '000000' . PHP_EOL;
}
public function regenerateAction(int $count = 0)
{
// This is the regenerate action
echo '111111' . PHP_EOL;
}
}We can then call the main action (default action):
./cli.php usersor the regenerate action:
./cli.php users regenerateParameters
You can also pass parameters to actions. An example of how to process the parameters can be found above, in the sample bootstrap file.
<?php
declare(strict_types=1);
namespace MyApp\Tasks;
use Phalcon\Cli\Task;
class UsersTask extends Task
{
public function mainAction()
{
echo '000000' . PHP_EOL;
}
public function addAction(int $first, int $second)
{
echo $first + $second . PHP_EOL;
}
}We can then run the following command:
php cli.php users add 4 5
9Parameters can also be accessed through the Phalcon\Cli\Dispatcher which is helpful when passing flags in or an
unknown number of parameters.
<?php
declare(strict_types=1);
namespace MyApp\Tasks;
use Phalcon\Cli\Task;
class UsersTask extends Task
{
public function mainAction()
{
print_r( $this->dispatcher->getParams() );
}
}Running this will then output:
php cli.php users main additional parameters
Array
(
[0] => additional
[1] => parameters
)Chain
You can also chain tasks. To run them one after another, we need to make a small change in our bootstrap: we need to register our application in the DI container:
// ...
$console = new Console($container);
$container->setShared('console', $console);
$arguments = [];
// ...Now that the console application is inside the DI container, we can access it from any task.
Assume we want to call the printAction() from the Users task, all we have to do is call it, using the container.
<?php
namespace MyApp\Tasks;
use Phalcon\Cli\Console;
use Phalcon\Cli\Task;
/**
* @property Console $console
*/
class UsersTask extends Task
{
public function mainAction()
{
# This is the default task and the default action
echo '000000' . PHP_EOL;
# Also handle the `print` action
$this->console->handle(
[
'task' => 'main',
'action' => 'print',
]
);
}
public function printAction()
{
# Print action executed also
echo '444444' . PHP_EOL;
}
}This technique allows you to run any task and any action from any other task. However, it is not recommended because it
could lead to maintenance nightmares. It is better to extend Phalcon\Cli\Task and implement your logic there.
Modules
CLI applications can also handle different modules, the same as MVC applications. You can register different modules in your CLI application to handle different paths of your CLI application. This allows for better organization of your code and grouping of tasks.
You can register a frontend and backend module for your console application as follows:
<?php
declare(strict_types=1);
use Exception;
use MyApp\Modules\Backend\Module as BackendModule;
use MyApp\Modules\Frontend\Module as FrontendModule;
use Phalcon\Autoload\Loader;
use Phalcon\Cli\Console;
use Phalcon\Cli\Dispatcher;
use Phalcon\Di\FactoryDefault\Cli as CliDI;
use Phalcon\Exception as PhalconException;
use Throwable;
$loader = new Loader();
$loader->setNamespaces(
[
'MyApp' => 'src/',
]
);
$loader->register();
$container = new CliDI();
$dispatcher = new Dispatcher();
$dispatcher->setDefaultNamespace('MyApp\Tasks');
$container->setShared('dispatcher', $dispatcher);
$console = new Console($container);
$console->registerModules(
[
'frontend' => [
'className' => FrontendModule::class,
'path' => './src/frontend/Module.php',
],
'backend' => [
'className' => BackendModule::class,
'path' => './src/backend/Module.php',
],
]
);
$arguments = [];
foreach ($argv as $k => $arg) {
if ($k === 1) {
$arguments['task'] = $arg;
} elseif ($k === 2) {
$arguments['action'] = $arg;
} elseif ($k >= 3) {
$arguments['params'][] = $arg;
}
}
try {
$console->handle($arguments);
} catch (PhalconException $e) {
fwrite(STDERR, $e->getMessage() . PHP_EOL);
exit(1);
} catch (Throwable $throwable) {
fwrite(STDERR, $throwable->getMessage() . PHP_EOL);
exit(1);
} catch (Exception $exception) {
fwrite(STDERR, $exception->getMessage() . PHP_EOL);
exit(1);
}The above code assumes that you have structured your directories to contain modules in the frontend and backend
directories.
src/
src/backend/Module.php
src/frontend/Module.php
php cli.phpModule Definitions
As of 5.14.2 the console processes module definitions the same way as Phalcon\Mvc\Application. A module can be
defined as an array (as shown above) or as a Closure. The closure receives the DI container as its only argument and
is invoked when the module starts:
<?php
declare(strict_types=1);
use MyApp\Modules\Backend\ReportWriter;
use Phalcon\Cli\Console;
use Phalcon\Di\DiInterface;
use Phalcon\Di\FactoryDefault\Cli as CliDI;
$container = new CliDI();
$console = new Console($container);
$console->registerModules(
[
'backend' => function (DiInterface $container) {
$container->setShared('reportWriter', ReportWriter::class);
},
]
);For array definitions, the class named by className is resolved from the container and its registerAutoloaders()
and registerServices() methods are called automatically. For closure definitions, the closure body is responsible
for any service registration; registerAutoloaders() and registerServices() are not called.
A module name that was never registered raises Phalcon\Application\Exceptions\ModuleNotRegistered. A definition that
is neither an array nor a Closure raises Phalcon\Cli\Console\Exceptions\InvalidModuleDefinition.
Methods
The CLI application offers the following methods:
public function getDefaultModule(): stringReturns the default module name
public function getModule(string $name): array | objectGets the module definition registered in the application via module name
public function getModules(): arrayReturn the modules registered in the application
public function registerModules(array $modules, bool $merge = false): AbstractApplicationRegister an array of modules present in the application
public function setDefaultModule(string $defaultModule): AbstractApplicationSets the module name to be used if the router does not return a valid module
Routes
The CLI application has its own router. By default, the Phalcon CLI application uses the Phalcon\Cli\Router object, but you can implement your own by using the Phalcon\Cli\RouterInterface.
Default Routes
Similar to an MVC application, the Phalcon\Cli\Router uses Phalcon\Cli\Router\Route objects to store the route information. You can always implement your own objects by implementing the Phalcon\Cli\Router\RouteInterface.
These routes support regex parameters, such as a-zA-Z0-9, and also provide additional placeholders:
| Placeholder | Description |
|---|---|
:module |
The module (need to set modules first) |
:task |
The task name |
:namespace |
The namespace name |
:action |
The action |
:params |
Any parameters |
:int |
Whether this is an integer route parameter |
The default routes are:
/:task/:action/:task/:action/:params
If you prefer not to use the default routes, you can disable them by passing false when constructing
the Phalcon\Cli\Router object:
<?php
declare(strict_types=1);
use Phalcon\Cli\Router;
$router = new Router(false);For more details about routes and route classes, you can refer to the Routing page.
Events
CLI applications in Phalcon are event-aware, allowing you to utilize the setEventsManager and
getEventsManager methods to access the events manager. The following events are available:
| Event | Stop | Description |
|---|---|---|
afterHandleTask |
Yes | Called after the task is handled. It allows you to perform actions after the task execution. |
afterStartModule |
Yes | Called after processing a module (if modules are used). Useful for post-processing tasks after a module is executed. |
beforeHandleTask |
No | Called before the task is handled. It provides an opportunity to perform actions before the task execution. |
beforeStartModule |
Yes | Called before processing a module (if modules are used). Useful for pre-processing tasks before a module is executed. |
boot |
Yes | Called when the application boots. It is useful for performing actions during the application’s bootstrapping process. |
If you are using the Phalcon\Cli\Dispatcher, you can also leverage the beforeException event, which
can stop operations and is fired from the dispatcher object.
These events offer hooks into different stages of the CLI application’s lifecycle, enabling you to execute custom logic at specific points in the application flow.
Exceptions
Any exception thrown in the Phalcon\Cli\Console component will be of type Phalcon\Cli\Console\Exception, which
allows you to trap the exception specifically. The one exception to this rule is an unregistered module name, which
raises Phalcon\Application\Exceptions\ModuleNotRegistered - a subclass of Phalcon\Application\Exception, shared
with Phalcon\Mvc\Application.
Granular Exceptions
As of 5.14 the CLI Console and Router raise granular subclasses of their respective Exception types so callers can
catch a specific failure mode. Existing catch (Phalcon\Cli\Console\Exception $e) /
catch (Phalcon\Cli\Router\Exception $e) blocks continue to work unchanged.
As of 5.14.2 module processing is aligned with Phalcon\Mvc\Application and two classes have been removed:
Phalcon\Cli\Console\Exceptions\ConsoleModuleNotRegisteredis replaced byPhalcon\Application\Exceptions\ModuleNotRegistered, which extendsPhalcon\Application\Exception- notPhalcon\Cli\Console\Exception. Acatch (Phalcon\Cli\Console\Exception $e)block no longer traps an unregistered module.Phalcon\Cli\Console\Exceptions\InvalidModuleDefinitionPathis replaced byPhalcon\Cli\Console\Exceptions\InvalidModuleDefinition.
Update any catch blocks that reference the removed classes.
| Class | Parent | Thrown when |
|---|---|---|
Phalcon\Application\Exceptions\ModuleNotRegistered |
Phalcon\Application\Exception |
A module name passed to handle() was never registered. |
Phalcon\Cli\Console\Exceptions\ContainerRequired |
Phalcon\Cli\Console\Exception |
The console is invoked without a DI container. |
Phalcon\Cli\Console\Exceptions\InvalidModuleDefinition |
Phalcon\Cli\Console\Exception |
A module definition is neither an array nor a Closure. |
Phalcon\Cli\Console\Exceptions\ModuleDefinitionPathNotFound |
Phalcon\Cli\Console\Exception |
A module definition path points at a file that does not exist. |
Phalcon\Cli\Router\Exceptions\BeforeMatchNotCallable |
Phalcon\Cli\Router\Exception |
A route beforeMatch callback is not callable. |
Phalcon\Cli\Router\Exceptions\InvalidRoutePaths |
Phalcon\Cli\Router\Exception |
Route paths cannot be processed to a routable array. |
Phalcon\Cli\Router\Exceptions\RouterArgumentsInvalidType |
Phalcon\Cli\Router\Exception |
Arguments passed to handle() are not a string or array. |