21.5 C
Pakistan
Saturday, June 15, 2024

Using PHP to interact with the OS process

Occasionally, you may be required to utilize OS-level commands within your PHP application. Let’s see if we can improve the Developer Experience by examining how we can achieve this.

I’ve been concentrating on different facets of my code writing and how I can make it better for the past few years. Initially, I investigated ways to improve and make HTTP integration more object-oriented. I think I’ve figured out how to do this, and I’m shifting my focus now.

There are times when you’ll want to work within your applications using the OS CLI. either in a different CLI application or a web application. We have previously used commands like exec, pass-thru, shell_exec, and system. We were saved when the Symfony Process component appeared.

It was very simple to integrate with OS processes and obtain the output thanks to the Symfony process component. However, there is still some frustration with the way we integrate with this library. In order to execute the command we want to run, we create a new process and pass in an array of arguments. Let’s examine this:

$command = new Process(
    command: ['git', 'push', 'origin', 'main'],
);
 
$command->run();

What about this method is flawed? Well, to be honest, not much. Is it possible for us to enhance the developer experience, though? Suppose we move from git to svn (which is probably not going to happen).

In order to enhance the developer experience, we must first comprehend the logical elements that comprise an OS command. These can be divided into:

executable parameters

Anything that we directly interact with, like php, git, brew, or any other installed binary on our system, is our executable. Then, the arguments represent possible ways for us to communicate; these could be flags, options, subcommands, or arguments.

Thus, with a little abstraction, we will have a command that accepts arguments and a process. Interfaces and contracts will be used to specify the components and regulate the operation of our workflow. First, let’s discuss the Process Contract:

declare(strict_types=1);
 
namespace JustSteveKing\OS\Contracts;
 
use Symfony\Component\Process\Process;
 
interface ProcessContract
{
    public function build(): Process;
}

This means that every process needs to be able to be built, and that the finished product of that creation should be a Symfony Process. Let’s look at our Command Contract now that our procedure should have created a command for us to execute:

declare(strict_types=1);
 
namespace JustSteveKing\OS\Contracts;
 
interface CommandContract
{
    public function toArgs(): array;
}

Our command’s primary goal is to be able to return arguments that we can use as commands when passing them into a Symfony Process.

Let’s move on from concepts and examine a practical example. Since most of us are familiar with git commands, we’ll use git as an example.

Let’s start by establishing a Git process that puts the recently discussed Process Contract into practice:

class Git implements ProcessContract
{
    use HandlesGitCommands;
 
    private CommandContract $command;
}

In addition to implementing the contract, our process has a command property that we will utilize to ensure smooth construction and operation. We possess a feature that will allow us to centralize the creation and building of things for our Git process. Now let’s examine that:

trait HandlesGitCommands
{
    public function build(): Process
    {
        return new Process(
            command: $this->command->toArgs(),
        );
    }
 
    protected function buildCommand(Git $type, array $args = []): void
    {
        $this->command = new GitCommand(
            type: $type,
            args: $args,
        );
    }
}

Thus, our characteristic demonstrates the execution of the process contract itself and offers guidelines for developing processes. Additionally, it has a technique that lets us abstract building commands.

Up until this point, we can develop a procedure and a possible command. But we still haven’t given an order. Using a Git class for the type, we create a new Git Command in the trait. Let’s examine this additional Git class, an enum. I will demonstrate a simplified version, though, since you should really want this to correspond to every git subcommand you want to support:

enum Git: string
{
    case PUSH = 'push';
    case COMMIT = 'commit';
}

Next, we forward this to the Git Command:

final class GitCommand implements CommandContract
{
    public function __construct(
        public readonly Git $type,
        public readonly array $args = [],
        public readonly null|string $executable = null,
    ) {
    }
 
    public function toArgs(): array
    {
        $executable = (new ExecutableFinder())->find(
            name: $this->executable ?? 'git',
        );
 
        if (null === $executable) {
            throw new InvalidArgumentException(
                message: "Cannot find executable for [$this->executable].",
            );
        }
 
        return array_merge(
            [$executable],
            [$this->type->value],
            $this->args,
        );
    }
}

We accept the arguments from our Process in this class; at the moment, our HandledGitCommands trait handles this. We can then translate this into arguments that are understandable to the Symfony Process. Utilizing the Symfony package’s ExecutableFinder, we can reduce path errors. However, in the event that the executable cannot be located, we also want to raise an exception.

When everything is combined inside our Git Process, it somewhat resembles this:

use JustSteveKing\OS\Commands\Types\Git as SubCommand;
 
class Git implements ProcessContract
{
    use HandlesGitCommands;
 
    private CommandContract $command;
 
    public function push(string $branch): Process
    {
        $this->buildCommand(
            type: SubCommand:PUSH,
            args: [
                'origin',
                $branch,
            ],
        );
 
        return $this->build();
    }
}
Now all that is left for us to do is run the code itself so that we can work with git nicely inside of our PHP application:

$git = new Git();
$command = $git->push(
    branch: 'main',
);
 
$result = $command->run();

You can engage with the Symfony Process as a result of the push method; that is, you can perform a variety of actions with the command that comes from the other end. The creation of an object-oriented wrapper around this process is the only thing we have changed. This extends things in a testable and extendable way and enables us to develop and maintain context nicely.

What is the frequency of your OS command usage in your applications? What applications come to mind for this? Terraform, Ansible, MySQL, and SSH are all great examples of this! What if you could effectively schedule MySQL dumps from Laravel Artistry without constantly relying on external packages?

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles