October 5, 2023

Execute code from another app with a completely different PHP version and Composer dependencies.

Communication between apps that are developed in different languages, versions and dependencies without using HTTP or AJAX.

In this blog, I’ll write about how you can execute code from one app to another, even if that app has a different PHP version and/or different Composer dependencies.

The most standard approach to data exchange between two separate apps would be to set up a route in one app and call that route from another app by making an HTTP or AJAX request. While this approach works great, it creates a lot of overhead and extra work to simply execute and fetch data from another environment. Also, as the app gets larger, it would be impossible to cover all possible use cases of fetching data by simply continuing to add new routes every time some new data needs to be retrieved.

Another approach is to extract logic from apps into common and shared Composer packages. This is a viable method and works if both apps have a lot of shared code that doesn’t rely on any unique third-party dependencies to function.

That said, in my experience, this way of separating shared code into packages creates a lot of overhead. Let’s say for example that you’re developing a new feature in a separate package. Firstly, it’s cumbersome to test that the new changes work in both apps. Secondly, after you’ve finished and pushed the new changes to your package, you also have to update it in both of your apps and deploy to get the new updates live.

Thinking there might be a better way, I got intrigued to see if there was another way of achieving code execution across apps. I tried executing code through the command line instead of setting up new routes and doing HTTP roundtrips.

While trying different approaches to executing code on the command line, I believe that I made some interesting discoveries that I’ll share in this article. The method that I’m introducing works as long as both apps can be cloned or pulled into the same environment. Let’s get right into it!

PHP on the command line

So we’re all familiar with that you can run PHP code on the command line. There’s the php -a command that opens an interactive prompt. There are also the php -r command for running arbitrary code and php -f for executing a file with PHP code content.

At first glance, it seems like if we were to execute code on the command line, the php -r command would be the one to use. However, using this command for anything else than rudimentary and trivial PHP code executions would most likely end up giving you a PHP syntax error.

The reason for this is how the shell will interpret the command that is fed into the command line. Also, the fact that you need to escape all quotes in your command inside php -r '<command>' ends up in multiple factors working against each other. More info about the php -r command and its potential flaws and hazards is available in the PHP documentation here: [link] under the “-r” command line options section. To quote the PHP documentation itself, it even says:

It is […] easy to run into trouble when trying to use variables (shell or PHP) in command-line code, or using backslashes for escaping, so take great care when doing so. You have been warned!

A better approach

To avoid shell interpolations and quote escape issues, we’ll use the php -f command instead and load the code from a temporarily created file. PHP has this convenient function called tmpfile() that creates a temporary file which is automatically removed once there aren’t any references left to the resource in the code.

So to demonstrate how this would work and be set up, I’ve prepared this repository, [link], that is available on GitHub. I also built a basic docker image that has PHP7 and PHP8 built and compiled into it, it’s available here: [link].

Since the code is pretty basic and simply created as a proof of concept there is not much in there, but the most important part that triggers the php -f command is located in the Proxy.php file.

// https://github.com/oliverlundquist/app-code-share/blob/4225eb7283dc14eb24316c8f17a41adc53850c20/php7app/App/Proxy.php#L23-L31
$tempfile = tmpfile();
fwrite($tempfile, $phpCommand);
$filepath = stream_get_meta_data($tempfile)['uri'];
$command  =  <<<COMMAND
    cd {$php8dir};
    {$php8bin} -f {$filepath}
COMMAND;
$result = exec($command, $output, $returnCode);

After loading the code that will be executed into a tmpfile, it executes it with php -f through the exec() function. This works because exec() changes the directory to the folder of the specified app and uses the version of the PHP binary that’s compatible with that app.

The code that is set to the $phpCommand variable and then passed into the tempfile could be any PHP-compatible code which makes it so flexible and powerful. Now one thing that is important to remember, when using exec() to execute PHP code on the command line, it’s important to echo out the content that you want the command to return. A simple return 'something' won’t give you any data back.

Let’s see this in action

Inside the repository on GitHub, I’ve prepared a docker-compose.yml file. To see how this works, start by cloning the repository at [link], go to the root of the folder and run docker-compose up to boot up a docker environment running this demo. This automatically starts a PHP server at http://localhost:8080.

Browse to [link] to see the first demo of this setup. It should display “ping pong”. Now while this doesn’t seem very exciting at first, when you look into the index.php file that is loaded, it gets a bit more interesting.

// https://github.com/oliverlundquist/app-code-share/blob/4225eb7283dc14eb24316c8f17a41adc53850c20/php7app/index.php#L16-L20
$command = <<<'COMMAND'
    use App\Features\POC\Ping;
    echo (new Ping)->execute();
COMMAND;
echo (new Proxy)->toPhp8App($command);

So what happens is that the PHP7 app first executes code on the PHP8 app. Next, inside the PHP8 app, code is executed on the PHP7 app to get the “pong” part of the string.

// https://github.com/oliverlundquist/app-code-share/blob/4225eb7283dc14eb24316c8f17a41adc53850c20/php8app/App/Features/POC/Ping.php#L11-L20
// get 'pong' from php7 app
$command = <<<'COMMAND'
    use App\Features\POC\Pong;
    echo (new Pong)->execute();
COMMAND;
$ping = 'ping';
$pong = (new Proxy)->toPhp7App($command);

// return 'ping pong'
return $ping . ' ' . $pong;

To summarize what happens, the PHP7 app calls the Ping feature in the PHP8 app, the Ping feature calls the Pong feature in the PHP7 app, concatenates the results with its own “ping” string and we get “ping pong” back.

Now this is only thought of as a proof of concept and isn’t really a useful example, so let’s look into a more realistic example.

A more useful example

Let us imagine that you have an old app that has been worked on for many years. At some point, it was decided that a new framework and platform should be introduced, development of new apps then continued on that platform. Now you’ve got two completely separate and independent apps on your hands. They both run different versions of PHP and they have different Composer dependencies that are not really possible or easy to merge.

After working hard at it, you’re just about to finish your new shining Campaigns app on the new platform and everything is going great. That is when you realize that you need a functionality that’s only available in the old app. You would save loads of time reusing this code instead of rewriting the whole thing into the new platform.

Let’s say that this feature in the old app is a feature that sends out emails to customers. This is exactly where this setup shines, let’s look at how this can be executed.

Inside the php8app folder where the new app is located, we’ll call the NotifyCustomerViaEmail feature in the old app like so:

// https://github.com/oliverlundquist/app-code-share/blob/4225eb7283dc14eb24316c8f17a41adc53850c20/php8app/App/Features/Campaigns/CreateCampaignForNewCustomers.php#L22-L31
$command = '
    use App\Features\Customers\NotifyCustomerViaEmail;
    use App\Features\SuccessfulExecution;
    $feature = (new NotifyCustomerViaEmail)->execute('. $customerId .');
    if ($feature instanceof SuccessfulExecution) {
        $feature = $feature->getResult()->toArray();
    }
    echo json_encode($feature);
';
$notifiedCustomers[] = json_decode((new Proxy)->toPhp7App($command));

As demonstrated in this example, you can write code just like you would when developing inside the original app. The output from this command is available as a demo here when the local docker environment is running: [link]

Conclusion

While this approach is not a silver bullet, it is a more flexible and less costly method of sharing and executing code between two completely different apps than for example setting up new endpoints and making HTTP requests.

This could be useful as a temporary measure when transcending into a more sustainable approach like merging the two apps or making them use the same PHP version and Composer dependencies.

Now, it goes without saying that you should always validate and sanitize data that is being passed into the command and between your apps.

That’s it for this time, I hope that this will be useful for someone out there wondering about alternative methods of sharing code between apps that don’t involve the normal approach of sending HTTP requests back and forth.

Until next time, have a good one!

Oliver Lundquist

Born in 🇸🇪, resident in 🇯🇵 for 13+ yrs, husband and father of a daughter and son, web developer since 2009.

Read more about me
• mail@oliverlundquist.com• Instagram (@olibalundo)