Fly.io is a great place to run both apps and workers! Deploy now on Fly.io, and get your Laravel app running globally in a few commands!
Laravel 10 has released the Laravel’s Process facade, which make running external commands super easily.
$result = Process::run("php -v");
echo $result->output();
This uses Symfony Process under the hood, and adds a TON of quality-of-life improvements.
What might you run commands for? I’ve personally used it to run docker
commands on an early version of Chipper CI, and recently have seen it used to run ffmpeg
commands to edit media files.
There’s a lot of times when it makes sense to reach out beyond PHP!
Let’s see how to run commands (processes), and learn some tricks along the way.
The Process Component
Some basics first! Let’s run the ls -lah
command to list out files in the current directory:
$result = Process::run('ls -lah')
The $result
is an instance of the Illuminate\Contracts\Process\ProcessResult
interface.
If you check that out, we’ll see a few handy methods available to us:
# See if it was successful
$result->successful(); // zero exit code
$result->failed(); // non-zero exit code
$result->exitCode(); // the actual exit code
$result->output(); // stdout
$result->errorOutput(); // stderr
Stdout and stderr
Output from a command will come in 2 flavors: stderr
and stdout
. They’re sort of like different channels that a command can use to report information, errors, or important data.
Did you know that stderr
isn’t just error messages? There’s some important stuff to know!
In general, stderr
output will be human-readable output meant to inform users about errors or just general information.
The stdout
output is generally meant to be something either human-readable or machine readbable. It might be something you’d ask some code to parse.
Let’s see that in action.
If we run fly apps list -j
to get a JSON format list of apps and their information, we may see this:
The informational message about the version of flyctl
and how to update is output to stderr
. The real content (the list of apps) is sent to stdout
.
The stdout
output is what we care about. Luckily, if I wanted to pipe the JSON output to jq
for some extra parsing, I could!
Even though the command outputs an informational message, sending output through a pipe will only pass along the stdout
content.
# Find the status of each app
# The pipe `|` only get stdout content
fly apps list -j | jq '.[].Status'
That lists the status of each Fly.io app listed.
Parsing Output in PHP
We can use this knowledge to parse command output in PHP.
// Get our list of apps as JSON and parse it in PHP
$result = Process::run("fly apps list -j");
if ($result->successful()) {
$apps = json_decode($result->output(), true);
var_dump($apps);
}
Here’s a fun issue with the above code: The flyctl
output has some debug information sent through stdout
, so we can’t directly parse the JSON outout!
It turns out that when running the above in a tinker session, the Laravel environment has LOG_LEVEL=debug
set.
While that is a Laravel-specific environment variable coming from the .env
file, flyctl
happens to use it also!
Why is the fly
command seeing that? This brings us to our next topic!
Environment Variables
Processes typically inherit their parent-processes environment variables. Since our PHP tinker session has environment variable LOG_LEVEL
set, that’s getting passed to the fly
command we ran.
The fly
command just so happens to use that environment variable, and so we get some debug information output (to stdout
in this case).
Luckily, we can unset that environment variable by setting it to false
:
# Unset the LOG_LEVEL env var when
# running the process:
$result = Process::env(['LOG_LEVEL' => false])
->run("fly apps list -j");
if ($result->successful()) {
$apps = json_decode($result, true);
// todo: Ensure JSON was parsed
foreach($apps as $app) {
echo $app['Name']."\n";
}
}
Now our output doesn’t contain the debug statements in stdout
and we can parse the JSON in PHP!
We’re not limited to unsetting environment variables. We can pass additional ones (or overwrite others) by passing more things to that array:
# If you want to see a TON of debug
# output from the `fly` command:
$result = Process::env([
'LOG_LEVEL' => 'debug',
'DEV' => '1',
])->run("fly apps list -j");
Streaming Output
What if we run a command that does a bunch of stuff over time, like deploying a Fly app?
We can actually get a command’s output in “real time”. We just pass the run
method a closure:
$result = Process::env('LOG_LEVEL' => false,)
->path("/path/to/dir/containing/a/fly.toml-file")
->run("fly deploy", function(string $type, string $output) {
// We'll ignore stderr for now
if ($type == 'stdout') {
doSomethingWithThisChunkOfOutput($output);
}
});
I’ve used this to stream output to users watching in the browser (with the help of Pusher and websockets).
Security
I’m not going to talk about sanitizing user input and the dangers of allowing user input to define what commands you might run (yeah, probably don’t do that).
Instead, I have something specific to Fly.io and Laravel! If you ran fly launch
to generate a Dockerfile, we have something to tweak here.
By default, the open_basedir
setting is set in the php-fpm
configuration.
This limits what directories PHP can see, which prohibits the ability for PHP to see (and run) commands in /usr/local/bin
or similar.
You change that, you can either remove that configuration or append a directory containing a command you’ll be using.
In file .fly/fpm/poold/www.conf
, I can append directory /usr/local/bin
:
; @fly settings:
; Security measures
php_admin_value[open_basedir] = /var/www/html:/dev/stdout:/tmp:/usr/local/bin
php_admin_flag[session.cookie_secure] = true
That change should get pulled in in your next deployment.
There’s More
There’s a bunch more you can do! Check out the docs and examples here!