• Formerly Platform.sh
  • Contact us
  • Docs
  • Login
Watch a demoFree trial
Blog
Blog
BlogProductCase studiesNewsInsights
Blog

Crafting Hybrid PHP-Go CLIs with Symfony Console

CLIGoPHPSymfony
01 February 2025
Antonis Kalipetis
Antonis Kalipetis
Cloud Engineer, Special Projects
Share
This post is also available in German and in French.

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.

The challenge: when your CLI needs to grow up

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.

Goals for the new CLI:

When we sat down to rethink our CLI strategy, we established three core goals:

1. Backward Compatibility

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.

2. Performance Without Sacrifice

We wanted a fast CLI that didn't compromise on functionality. No more waiting for runtime initialization or dealing with slow startup times.

3. Universal Accessibility

The CLI should work for everyone, regardless of what languages they have installed on their machine. A single binary that just works.

Choosing Go for the new CLI

After evaluating our options, Go emerged as the clear winner for several reasons:

  • Single Binary Distribution: Go compiles to a single binary with no external dependencies. Download, make executable, run, that's it. Seconds instead of minutes for setup.
  • Cross-Platform Compilation: From my Mac, I can compile binaries for Windows, Linux, and different architectures – all with the same codebase and toolchain.
  • Performance: Go is fast and has become the go-to language for developer tooling. Tools like Terraform and many other CLI tools are written in Go.
  • No Runtime Dependencies: This was the big one – no more requiring users to install PHP.

Embedding PHP without requiring PHP

Here's where it gets interesting. Instead of rewriting everything from scratch, we chose a hybrid approach that gave us immediate benefits:

Embedding legacy code

We embedded our existing PHP CLI into the new Go binary. This meant:

  • Instant backward compatibility – everything that worked before continues to work
  • Same user experience – no need to retrain users or update documentation
  • Gradual migration – we could migrate commands one by one as needed

Building a minimal PHP binary

The technical challenge was: how do you run PHP code from Go without requiring PHP to be installed? Our solution was elegant:

  1. Build a minimal PHP binary with only the necessary extensions for our CLI.
  2. Compile this minimal binary for each target platform.
  3. Embed this binary into our Go CLI.
  4. Route commands to either PHP (legacy) or Go (new) as needed.

Yes, it was time-consuming to set up, but you only do it once.

Command routing made simple

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.

Real results: The good, bad, and ugly

The good

  • Single binary distribution – users love the simplicity
  • Easy embedding of Go tools – we can include other Go-based tools directly in our CLI
  • Code reusability – our Go code can be reused in SDKs and other projects
  • Better testing – Go's testing ecosystem made our end-to-end testing much more robust

The bad

  • Dual maintenance – we now maintain both legacy PHP code and new Go code
  • Release complexity – coordinating releases across two codebases requires careful planning
  • Developer laziness – most functionality still lives in the PHP side because it's easier to maintain existing code than rewrite it

The ugly

  • Symfony shortcuts – PHP's Symfony Console supports command shortcuts (like app:init becoming a:i), but this doesn't work seamlessly across the hybrid boundary
  • Autocomplete challenges – getting autocomplete to work properly across two CLI systems required significant effort.

Switching to the Symfony console in Go

Here'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.

Why this was perfect for us

  • Same UX/DX – our legacy CLI used Symfony Console in PHP, so using the Go version meant an identical user experience.
  • Argument parsing – all the complex parsing logic worked the same way.
  • Command routing – we could handle both legacy and new commands in a centralized way.

Implementation: making it work

Our new architecture with Symfony Console for Go works like this:

  1. Load all commands (both PHP legacy and Go new) into the Go-based Symfony Console
  2. Route intelligently – determine whether to execute in PHP or Go based on the command
  3. Parse and validate everything on the Go side before execution
  4. Provide unified autocomplete and shortcuts across all commands

Command Routing in Action

# 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 Go

The migration path

Our 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.

Testing without breaking production

When you're handling users' production code and deployment pipelines, reliability is paramount. Our testing strategy includes:

Integration testing

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.

Unit testing

Standard Go unit testing for new commands and functionality.

End-to-End testing

Go made this much easier – we can spin up mock servers and test complete workflows without external dependencies.

What's next?

We're not stopping here. Our roadmap includes:

Authentication improvements

Moving all authentication logic to Go for better performance and security. Go's ecosystem offers better support for secure credential storage than PHP.

Plugin system

We're exploring ways to enable users to extend our CLI with their own custom commands, potentially written in Go and integrated seamlessly.

Complete migration

While we'll always support legacy commands, we're gradually moving core functionality to Go for better performance and maintainability.

Live demo

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.

Key takeaways for your projects

If you're facing a similar challenge, here are the lessons we learned:

  1. Don't fear hybrid approaches – you don't have to rewrite everything at once
  2. Leverage existing solutions – the Symfony Console Go implementation saved us months of work
  3. Prioritize user experience – backward compatibility is often more critical than technical purity
  4. Plan for gradual migration – big bang rewrites are risky; incremental change is safer
  5. Test extensively – when users depend on your tools for production workflows, reliability is non-negotiable

Wrapping up

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.

Stay updated

Subscribe to our monthly newsletter for the latest updates and news.

Your greatest work
is just on the horizon

Free trial
UpsunFormerly Platform.sh

Join our monthly newsletter

Compliant and validated

ISO/IEC 27001SOC 2 Type 2PCI L1HIPAATX-RAMP
© 2025 Upsun. All rights reserved.