Support the ongoing development of Laravel.io →

Console Applications With Laravel Zero

22 May, 2023 7 min read

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 with use($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!

Last updated 2 weeks ago.

driesvints, abdessamadbettal liked this article

2
Like this article? Let the author know and give them a clap!

Other articles you might like

November 18th 2024

Laravel Custom Query Builders Over Scopes

Hello 👋 Alright, let's talk about Query Scopes. They're awesome, they make queries much easier to r...

Read article
November 19th 2024

Access Laravel before and after running Pest tests

How to access the Laravel ecosystem by simulating the beforeAll and afterAll methods in a Pest test....

Read article
November 11th 2024

🍣 Sushi — Your Eloquent model driver for other data sources

In Laravel projects, we usually store data in databases, create tables, and run migrations. But not...

Read article

We'd like to thank these amazing companies for supporting us

Your logo here?

Laravel.io

The Laravel portal for problem solving, knowledge sharing and community building.

© 2024 Laravel.io - All rights reserved.