For a quick read-through of the main takeaways, keep scrolling for our distilled write-up. We utilized ChatGPT to enhance the grammar and syntax.
When you’ve been running a developer-oriented platform for over a decade, your tooling tends to accumulate some baggage. We found ourselves with a command-line interface (CLI) fully implemented in PHP—a natural choice at the time, given our strong ties to the PHP and Drupal ecosystems. However, as our audience and supported languages expanded, we realized we needed to rethink how our CLI was built and distributed.
The result? A hybrid CLI that wraps our existing PHP CLI logic inside a new Go-based binary, brought together with Symfony Console for consistent command routing, autocomplete, and shortcuts. Below is our story of how we got there, why we made certain decisions, and what we learned along the way.
Originally, our CLI was written in PHP, perfectly suitable for our primary user base at the time. As we grew, so did our users’ tech stacks: Node.js, Python, Go, Ruby, Symfony, and more. We needed to deliver a CLI experience to teams who might not have PHP installed—nor did they want to install it just to run one tool.
Key goals that pushed us to reconsider our CLI approach:
Go is widely used in the developer tooling ecosystem (e.g., Docker, Terraform and countless other command-line tools). It compiles to a single static binary that runs quickly and is easy to distribute for Linux, macOS, and Windows. These were our primary motivators:
We still needed all our existing PHP code—thousands of lines’ worth of logic—available to users. But how do we run that inside a Go binary without requiring separate PHP on the user’s machine?
This setup meant we didn’t have to rewrite all existing commands immediately. By embedding PHP, everything kept working, but we could still add fresh functionality in Go without holding up the entire release.
We relied on Symfony Console in our original PHP-based CLI for arguments, shortcuts, and autocompletion. We wanted to keep that familiar user experience—shortcuts like p:init
for project:init
, or well-structured help texts, etc.
It turns out the official Symfony CLI is written in Go, and it includes a Go-based Symfony Console component. By swapping our prior Go library (Cobra) with this Symfony Console for Go, we gained:
project:init
, validate
) in the new Go code.Of course, maintaining two codebases (the embedded PHP for old commands and new Go code) takes extra work. But it also gives us the freedom to move at our own pace, ensuring each feature is thoroughly tested without forcing a massive, big-bang rewrite.
We never wanted to break developers’ production workflows:
We’re steadily migrating more commands to Go, especially those requiring authentication. Once that’s complete, we can fully unify how credentials and tokens are managed across the new binary—potentially improving SSH key handling and secure storage integration. We’re also exploring ways to let third-party developers extend the CLI, now that the codebase is more modular.
Rewriting a mature CLI is rarely simple—especially when thousands of developers rely on it daily for production workflows. By adopting a hybrid architecture, we kept existing PHP-based functionality, drastically improved distribution (single binary, no language dependencies), and laid a foundation for future expansions in Go.
Key takeaways:
We hope this journey might inspire you if you’re facing a similar legacy-versus-future dilemma. Whether you’re exploring new languages, new distribution models, or ways to unify your developer experience, a hybrid CLI might be exactly what your team needs.