This article is based on a presentation by Antonis Kalipetis, Staff Engineer at Upsun, delivered at SymfonyCon 2024 in Vienna. We utilized AI tools for transcription and to enhance the grammar and syntax.
At SymfonyCon 2024 in Vienna, Antonis, a staff engineer at Upsun, shared the story of how Upsun rebuilt a command-line interface as a single, portable binary that still runs years of PHP commands. The twist: the new CLI is written in Go, routes both legacy and new commands, and uses Symfony console for a consistent developer experience.
Upsun has been around for over 10 years, and in our early days, we were laser-focused on the PHP and Drupal communities. Naturally, our CLI was written in PHP; it made perfect sense. We could reuse existing code, share shortcuts with our users, and everything worked smoothly within our PHP ecosystem.
But then came the inevitable question that many growing companies face: "Do I really need to install PHP just to use a CLI tool?"
This hit us hard when we started targeting more ecosystems beyond PHP. Imagine a React developer or a Python developer being told they need to install PHP just to interface with our platform. It's not just about PHP – nobody wants to install any language runtime just to use a tool.
When we sat down to rethink our CLI strategy, we established three core goals:
We couldn't break existing workflows. Every command that worked with our old PHP CLI needed to continue working seamlessly. Our users had CI systems, scripts, and muscle memory built around our existing commands.
We wanted a fast CLI that didn't compromise on functionality. No more waiting for runtime initialization or dealing with slow startup times.
The CLI should work for everyone, regardless of what languages they have installed on their machine. A single binary that just works.
After evaluating our options, Go emerged as the clear winner for several reasons:
Here's where it gets interesting. Instead of rewriting everything from scratch, we chose a hybrid approach that gave us immediate benefits:
We embedded our existing PHP CLI into the new Go binary. This meant:
The technical challenge was: how do you run PHP code from Go without requiring PHP to be installed? Our solution was elegant:
Yes, it was time-consuming to set up, but you only do it once.
Our hybrid CLI works as a smart router:
User Input → Go CLI → Route Decision → Execute in PHP or Go
Command Loading: We load all commands (both legacy and new) on the Go side, providing a comprehensive view of available functionality.
Parsing and Validation: All argument parsing and validation occur in Go, allowing us to catch errors before they reach PHP.
Execution: Based on the command, we either execute Go code directly or shell out to our embedded PHP binary.
app:init becoming a:i), but this doesn't work seamlessly across the hybrid boundaryHere's where our story takes an interesting turn. Did you know the Symfony CLI itself is written in Go? This was a revelation that completely changed our approach.
The Symfony team had already solved the problem of implementing Symfony Console's functionality in Go. All those shortcuts, autocompletion features, and command routing – it's all there in Go, open source and ready to use.
Our new architecture with Symfony Console for Go works like this:
# Legacy PHP command - routed to embedded PHP
upsun project:list
# New Go command - executed natively
upsun project init
# Shortcuts work for both!
upsun p:l # Routes to PHP
upsun p init # Routes to GoOur rollout strategy was intentionally gradual:
Phase 1: Legacy CLI only (where we started).
Phase 2: Hybrid CLI with embedded legacy code.
Phase 3: Symfony Console integration for unified routing.
Phase 4: Gradual command migration from PHP to Go
This approach meant we never had a "big bang" moment where everything could break. Users could adopt the new CLI at their own pace, and we could migrate functionality incrementally.
When you're handling users' production code and deployment pipelines, reliability is paramount. Our testing strategy includes:
We run the same commands through both the PHP version and the embedded version, ensuring identical outputs. If a command works in the old CLI, it must work exactly the same way in the new one.
Standard Go unit testing for new commands and functionality.
Go made this much easier – we can spin up mock servers and test complete workflows without external dependencies.
We're not stopping here. Our roadmap includes:
Moving all authentication logic to Go for better performance and security. Go's ecosystem offers better support for secure credential storage than PHP.
We're exploring ways to enable users to extend our CLI with their own custom commands, potentially written in Go and integrated seamlessly.
While we'll always support legacy commands, we're gradually moving core functionality to Go for better performance and maintainability.
During the presentation, I showed our CLI in action:
# Original CLI
upsun project init # Works
# New hybrid CLI
u project init # Also works
# Symfony shortcuts now work across both!
u p init # This now works too!The shortcuts and autocompletion work seamlessly across both legacy PHP commands and new Go commands, thanks to Symfony Console handling all the routing logic.
If you're facing a similar challenge, here are the lessons we learned:
Building a hybrid PHP-Go CLI taught us that sometimes the best solution isn't the most obvious one. By embracing both languages and leveraging Symfony Console's Go implementation, we created a tool that's faster, more accessible, and more maintainable than what we had before.
The CLI is open-source – you can find it on GitHub under Upsun/cli – so feel free to explore our implementation and borrow ideas for your own projects.
Sometimes the best way forward isn't to abandon your legacy, but to build bridges that honor your past while embracing your future.
Want to learn more? The Upsun CLI is open source, and we're always happy to discuss our approach. You can find us at Upsun or check out our GitHub repository to see exactly how we implemented this hybrid approach.
Join our monthly newsletter
Compliant and validated