Console Applications With Laravel Zero
Photo by Mohammad Rahmani on Unsplash
Here's a news flash: You can write console applications with Laravel! Actually, it's kinda with Laravel and kinda without. Let's take a closer look at Laravel Zero and some cool things you might want to know about!
Support for Facades
First off: there's support for Facades, like Process
, Http
, or Log
. This enables us to fire off api calls with one command. Here's an example from an upcoming package that shows how to get a list of the organizations you've made on Fly.io:
Firstly, we'll need to authenticate, just like flyctl would. To do this we can ask flyctl to print the token it uses by running fly auth token
. Create a new command with php application make:command ExampleCommand
and add this in the handle()
method:
public function handle()
{
try
{
$result = Process::run('fly auth token')->throw();
$token = $result->output();
$this->info("got token successfully");
}
catch (ProcessFailedException $e)
{
$this->error($e->result->errorOutput());
return Command::FAILURE;
}
return Command::SUCCESS;
}
I've wrapped everything in a try-catch block so whenever a Process::run
method fails, it throws an error (the →throw()
method after Process::run()
takes care of that) and that error gets caught, prints out what went wrong and makes sure to return Command:FAILURE
. This way we can jsut assume everything went smoothly, and if not the catch
block will pick it up! This beats having to write an if-else
block for every Process:run
to check if it was successful or not, and print out the error if it wasn't.
Now that we have the auth token let's do something with it, shall we? We can get all your organizations on Fly.io like this:
try
{
$result = Process::run('fly auth token')->throw();
$token = $result->output();
$this->info("got token successfully");
+ $response = Http::withToken($token)
+ ->acceptJson()
+ ->contentType("application/json")
+ ->post('https://api.fly.io/graphql', ["query" => "query {currentUser {email} organizations {nodes{id slug name type viewerRole}}}"]);
+
+ // organizations will be an array of arrays that look like this: array("id" => , "slug" => , "name" => , "type" => , "viewerRole" => )
+ $organizations = $response->collect("data.organizations.nodes")->toArray();
+ $this->info("get organizations successfully");
}
Notice you can use the HTTP facade, just like you're already used to in Laravel? It'll be convenient like this all the way, which is just 👨🍳💋! You just have to install it before using it, or you'll get a BindingResolutionException
. Just run php <your-app-name> app:install http
and you're set!
Creating tasks
A wise man once said : "Console applications are only as good as their output looks". Okay, that's probably completely false, but still… Let's compare the outputs of our example app with how they could look:
The bottom one looks better, right? This is done by defining tasks. Each tasks has a title and an anonymous function. "loading…" is displayed when the function is executing, and it updates to a ✔ when the function returns true
or to a ❌ when it returns false
. Sweet, right?
These anonymous functions can introduce scoping issues if you're not careful, though! By default, anonymous functions in PHP do not share scope with the parent where they're defined. This means that we can't use variables that are defined outside of the anonymous function. Luckily, there's the use
keyword which is very useful (get it?): it can share variables from the outside into the anonymous function. There are two ways to mitigate this:
-
use ($variable)
: the value of $variable can be updated, but the parent scope won't inherit the changes made to it inside the anonymous function. Let me make this extra clear:
$variable = 1; // value is 1
$this->task("Scoping example", function () use($variable) {
$this->info($variable); // will print '1' to console output
$variable = 23; // value becomes 23
$this->info($variable); // will print '23' to console output
}); // changes made to $variable will be forgotten here
$this->info($variable); // this will print '1' to console output
So basically what use($variable)
does is copy over the value from the parent's $variable
into a new $variable
inside the function. The changes inside the function aren't copied over!
-
use(&$variable)
: the value of$variable
will be shared between the parent and the anonymous function. This is what you want to use if you want to keep the changes done to$variable
inside the anonymous function. Let's compare this withuse($variable)
:
$variable = 1; // value is 1
$this->task("Scoping example", function () use(&$variable) { // notice the '&' added here
$this->info($variable); // will print '1' to console output
$variable = 23; // value becomes 23
$this->info($variable); // will print '23' to console output
}); // changes made to $variable will NOT be forgotten here
$this->info($variable); // this will print '23' to console output, instead of '1'!
This is the difference by passing by value like use ($variable)
) and passing by reference like use(&$variable)
.
Here's how the tasks look when accounting for possible scoping issues:
+ $token = "";
+ $this->task("Getting token", function() use (&$token) {
$result = Process::run('fly auth token')->throw();
$token = $result->output();
+ });
+ $organizations = [];
+ $this->task("Getting organizations", function () use($token, &$organizations) {
$response = Http::withToken($token)
->acceptJson()
->contentType("application/json")
->post('https://api.fly.io/graphql', ["query" => "query {currentUser {email} organizations {nodes{id slug name type viewerRole}}}"]);
// organizations will be an array of arrays that look like this: array("id" => , "slug" => , "name" => , "type" => , "viewerRole" => )
$organizations = $response->collect("data.organizations.nodes")->toArray();
+ });
Creating interactive menus
There are a whole host of cool features to show, but I'll wrap it up here with interactive menus. Let's say I was creating a package to deploy apps on Fly.io with just one command (I am) and I needed to ask the user in what organization they want to deploy in (I need to). Then I'd get all the organizations like I showed above (I do) and show them in a menu for you to select from (I will). Here's how I did exactly that:
+ $selectedOrg = $this->menu("Select Organization", array_column($organizations, 'name'))
+ ->open();
+
+ if ($selectedOrg =="")
+ {
+ // user selected "Exit"
+ $this->info("Exiting.");
+ }
+ else
+ {
+ // user selected an organization
+ $this->info("Selected Org Name: ");
+ $this->info($organizations[$selectedOrg]['name']);
+ }
}
catch (ProcessFailedException $e)
This creates a menu in the command line (!) that looks like this (!!!) :
But the craziness doesn't stop there, oh no! There's a whole bunch of customization possible here. I can make that same menu look like this:
Here's how:
$selectedOrg = $this->menu("Select Organization", array_column($organizations, 'name'))
+ ->setForegroundColour('magenta')
+ ->setBackgroundColour('white')
+ ->setWidth(200)
+ ->setPadding(10)
+ ->setMargin(5)
+ ->setExitButtonText("Abort")
+ // remove exit button with
+ // ->disableDefaultItems()
+ ->setTitleSeparator('*-')
+ ->addLineBreak('<3', 2)
->open();
if ($selectedOrg =="")
{
// user selected "Exit"
$this->info("Exiting.");
}
else
{
// user selected an organization
$this->info("Selected Org Name: ");
$this->info($organizations[$selectedOrg]['name']);
}
}
catch (ProcessFailedException $e)
Packaging it up
Those were just some of the possibilities of Laravel Zero. I'll leave you with some quick notes about building the console application: with php <application-name> run:build <build-name>
you can build a standalone application. Do note that it requires PHP to be installed on the client.
Another way to use the commands you've built is by using composer to require your package. I didn't want to dirty Packagist with my in-development package, so here's how I added it using github instead:
// add this in the lowest level in composer.json, on the same level as 'name', 'license' and 'require'.
"repositories": [
{
"type": "vcs",
"url": "https://github.com/<organization name here>/<repo name here>"
}
],
Then, you can run composer require <organization name>/<repo name>
to install the latest release on the github project.
I'll stop typing now. I hope this has been useful, if there's ever some small to medium size task you want to automate, think about Laravel Zero!
driesvints, abdessamadbettal liked this article