# Working on legacy code: Installing multiple PHP Versions

*<s>I used AI to help me arrange my thoughts and sentences better</s>*

It was supposed to be a straightforward project. The client had an existing Laravel 5.7 application that needed a few new features. Simple enough, right? I had just set up my shiny new development environment with Laravel Herd, sporting the latest PHP 8.3. I cloned the repository, ran `composer install`, and then it hit me — the first of many error walls I'd face:

```plaintext
Your lock file does not contain a compatible set of packages. Please run composer update.

Problem 1
- Root composer.json requires php ^7.1.3 but your php version (8.3.11) does not satisfy that requirement.
Problem 2
- beyondcode/laravel-websockets is locked to version 1.3.0 and an update of this package was not requested.
- beyondcode/laravel-websockets 1.3.0 requires php ^7.1 -> your php version (8.3.11) does not satisfy that requirement.
...
```

And just like that, I found myself standing at the edge of the PHP version jungle, a place where many developers before me had gotten lost.

## The Project Reality

The project wasn't budgeted for a complete upgrade to PHP 8.x. The client simply needed a few feature additions to their existing application. "Can't you just make it work?" they asked. I've been in this game long enough to know that "just make it work" often hides a world of technical complexity.

In the past, I'd handled similar situations in a piecemeal fashion - installing a specific PHP version for a specific project and then figuring out how to switch back and forth. It was always messy, always time-consuming, and I vowed "never again." This time, I wanted a permanent solution.

## Considering the Options

I sat down with a cup of coffee and considered my options:

1. **Update the application to use PHP 8.3**  
    The ideal solution in theory, but estimated time to upgrade all those dependencies and fix compatibility issues? Days, if not weeks. The client's budget had no room for that.
    
2. **Use Laravel Herd's PHP version management**  
    While I love Herd for its simplicity, the paid version wasn't in the budget, and it didn't offer the flexibility I needed across multiple projects.
    
3. **Containerize with Docker**  
    A solid approach for many, but my aging laptop already wheezed when running multiple Docker containers. I needed something lightweight for daily development.
    
4. **Virtual Machines**  
    Another robust option, but again, too resource-intensive for my needs.
    
5. **Install multiple PHP versions natively**  
    This seemed like the most straightforward approach. Ubuntu's Ondrej PPA makes it easy to install multiple PHP versions side by side. But then, how would I switch between them seamlessly?
    

I decided to go with option 5 and build my own PHP version switcher. This would future-proof my workflow while keeping resource usage minimal.

## The Installation Adventure

First, I needed to install all the PHP versions I might encounter in the wild. The Ondrej PPA is a gift to PHP developers on Ubuntu:

```bash
sudo add-apt-repository ppa:ondrej/php
sudo apt update
```

Then came the big installation command. I wanted to be thorough, so I included all the common extensions I typically use for Laravel development:

```bash
sudo apt install \
php7.0-cli php7.0-mbstring php7.0-xml php7.0-curl php7.0-mysql php7.0-gd php7.0-zip php7.0-bcmath php7.0-intl php7.0-sqlite3 \
php7.1-cli php7.1-mbstring php7.1-xml php7.1-curl php7.1-mysql php7.1-gd php7.1-zip php7.1-bcmath php7.1-intl php7.1-sqlite3 \
php7.2-cli php7.2-mbstring php7.2-xml php7.2-curl php7.2-mysql php7.2-gd php7.2-zip php7.2-bcmath php7.2-intl php7.2-sqlite3 \
php7.3-cli php7.3-mbstring php7.3-xml php7.3-curl php7.3-mysql php7.3-gd php7.3-zip php7.3-bcmath php7.3-intl php7.3-sqlite3 \
php7.4-cli php7.4-mbstring php7.4-xml php7.4-curl php7.4-mysql php7.4-gd php7.4-zip php7.4-bcmath php7.4-intl php7.4-sqlite3 \
php8.0-cli php8.0-mbstring php8.0-xml php8.0-curl php8.0-mysql php8.0-gd php8.0-zip php8.0-bcmath php8.0-intl php8.0-sqlite3 \
php8.1-cli php8.1-mbstring php8.1-xml php8.1-curl php8.1-mysql php8.1-gd php8.1-zip php8.1-bcmath php8.1-intl php8.1-sqlite3 \
php8.2-cli php8.2-mbstring php8.2-xml php8.2-curl php8.2-mysql php8.2-gd php8.2-zip php8.2-bcmath php8.2-intl php8.2-sqlite3 \
php8.3-cli php8.3-mbstring php8.3-xml php8.3-curl php8.3-mysql php8.3-gd php8.3-zip php8.3-bcmath php8.3-intl php8.3-sqlite3
```

It was like preparing for a long trek through the jungle - you need to bring everything you might possibly need. As the terminal filled with scrolling text, I knew I was committed to this path.

## Crafting the PHP Version Switcher

Now came the creative part - building a script that would let me switch PHP versions with ease. I wanted something intuitive, something that would:

1. Let me list all available PHP versions
    
2. Allow me to switch between them with a simple command
    
3. Make the `php` command automatically use my selected version
    
4. Let me integrate Laravel Herd if I needed to
    

I created a file at `~/bin/php-switch` and filled it with my switcher script. It wasn't fancy, but it did the job:

```bash
#!/bin/bash

# Configuration directory
CONFIG_DIR="$HOME/.php-versions"
CURRENT_VERSION_FILE="$CONFIG_DIR/current"
PHP_VERSIONS_DIR="$CONFIG_DIR/versions"

# Create config directory if it doesn't exist
mkdir -p "$PHP_VERSIONS_DIR"

# Detect Herd installation
HERD_PHP_PATH="$HOME/.config/herd-lite/bin/php"

# Helper functions for listing, adding, removing and using PHP versions
# ... rest of the script ...
```

After making it executable with `chmod +x ~/bin/php-switch`, I added some magic to my `.bashrc` file:

```bash
# PHP version switcher
export PATH="$HOME/bin:$PATH"

# Enhanced php function with multiple alias support
php() {
  # Handle "php switch" command format
  if [ "$1" = "switch" ]; then
    shift
    "$HOME/bin/php-switch" "$@"
    return
  fi
  
  # Handle "php use <version>" shortcut
  if [ "$1" = "use" ] && [ -n "$2" ]; then
    "$HOME/bin/php-switch" use "$2"
    return
  fi
  
  # Use the currently selected PHP version
  if [ -f "$HOME/.php-versions/current" ]; then
    version=$(cat "$HOME/.php-versions/current")
    
    if [ "$version" == "herd" ]; then
      "$HOME/.config/herd-lite/bin/php" "$@"
    else
      "$(cat "$HOME/.php-versions/versions/$version")" "$@"
    fi
  else
    # Default to Herd PHP if no version is selected
    "$HOME/.config/herd-lite/bin/php" "$@"
  fi
}
```

This gave me the ability to use commands like `php switch list`, `php use 7.1`, or simply `php -v` with the correct version automatically used.

After registering all my PHP versions:

```bash
php switch add herd ~/.config/herd-lite/bin/php
php switch add 7.0 /usr/bin/php7.0
php switch add 7.1 /usr/bin/php7.1
php switch add 7.2 /usr/bin/php7.2
# ... and so on
```

I felt ready to tackle my project. A quick `php use 7.1` and I was running the correct PHP version:

```plaintext
okonu@vulcan:~$ php use 7.1
Switched to PHP version: 7.1 (7.1.33-67+ubuntu24.04.1+deb.sury.org+1)
```

## The Composer Conundrum

Victory was short-lived. As I tried to install the project dependencies:

```plaintext
okonu@vulcan:~/code/project$ composer i
Composer 2.3.0 dropped support for PHP <7.2.5 and you are running 7.1.33-67+ubuntu24.04.1+deb.sury.org+1, please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.
```

I found myself in a catch-22: I needed an older Composer version to work with PHP 7.1, but couldn't downgrade Composer because I was already using PHP 7.1! When I tried:

```plaintext
okonu@vulcan:~/code/project$ composer self-update --2.2
Composer 2.3.0 dropped support for PHP <7.2.5 and you are running 7.1.33-67+ubuntu24.04.1+deb.sury.org+1, please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.
```

It was like being told I needed a key to open a box that contained the key I needed.

The solution? I needed to use PHP 7.4 (which can run Composer 2.3) to install Composer 2.2 (which can run on PHP 7.1):

```bash
sudo curl -sS https://getcomposer.org/installer | sudo php7.4 -- --install-dir=/usr/local/bin --filename=composer-php71 --version=2.2.18
sudo chmod +x /usr/local/bin/composer-php71
```

Then I enhanced my Composer wrapper in `.bashrc`:

```bash
function composer() {
  php_version=$(php -r "echo PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;")
  if (( $(echo "$php_version < 7.2" | bc -l) )); then
    /usr/local/bin/composer-php71 "$@"
  else
    /usr/local/bin/composer "$@"
  fi
}
```

This automatically selects the right Composer version based on the active PHP version.

## Dependency Hell: The Final Boss

Now I was ready to finally install the project dependencies. But then:

```plaintext
okonu@vulcan:~/code/project$ /usr/bin/php7.1 $(which composer-php71) install
...
  Problem 1
    - egulias/email-validator is locked to version 3.2.1 and an update of this package was not requested.
    - egulias/email-validator 3.2.1 requires php >=7.2 -> your php version (7.1.33) does not satisfy that requirement.
  Problem 2
    - facade/ignition-contracts is locked to version 1.0.2 and an update of this package was not requested.
    - facade/ignition-contracts 1.0.2 requires php ^7.3|^8.0 -> your php version (7.1.33) does not satisfy that requirement.
...
```

The composer.lock file contained dependencies that required newer PHP versions than the project's primary requirement! This happens when a project is initially developed on PHP 7.1, but later some dependencies are updated without testing compatibility.

The solution was to update the dependencies to versions compatible with PHP 7.1:

```bash
/usr/bin/php7.1 $(which composer-php71) update --with-dependencies
```

The terminal filled with a cascade of downgraded packages:

```plaintext
  - Removing league/mime-type-detection (1.11.0)
  - Removing symfony/deprecation-contracts (v2.5.2)
  - Downgrading asm89/stack-cors (v2.1.1 => v2.0.5)
  - Upgrading aws/aws-crt-php (v1.2.4 => v1.2.7)
  - Upgrading aws/aws-sdk-php (3.263.13 => 3.278.3)
  - Upgrading defuse/php-encryption (v2.3.1 => v2.4.0)
  - Upgrading doctrine/deprecations (1.1.3 => 1.1.4)
  - Upgrading doctrine/instantiator (1.4.1 => 1.5.0)
  - Downgrading egulias/email-validator (3.2.1 => 2.1.25)
  - Downgrading facade/ignition-contracts (1.0.2 => 1.0.1)
  - Upgrading filp/whoops (2.14.6 => 2.18.0)
  ...
```

And just like that, after battling through the PHP version jungle, I had conquered the project setup. The application was now running on PHP 7.1 with compatible dependencies, ready for me to add the new features the client needed.

## What I Learned

This journey through the PHP version jungle taught me several important lessons:

1. **Plan for compatibility**: It's always worth having a system for managing multiple PHP versions if you work with a range of projects.
    
2. **Composer has version requirements too**: The Composer tool itself has PHP version requirements that can trip you up when working with older PHP versions.
    
3. **Dependencies have a version chain**: A project might specify PHP 7.1 as the requirement, but its dependencies might require newer PHP versions. Always check the full dependency tree.
    
4. **Simple solutions work best**: While containerization is powerful, sometimes a simple bash script provides the most efficient solution for day-to-day development.
    
5. **Future-proofing saves time**: The time I invested in creating a robust PHP version management system will save me countless hours in the future.
    

## What's Next?

With more time and perhaps some funding, this simple PHP version switcher could evolve into a proper open-source tool similar to nvm for Node.js. Features might include:

* Project-level PHP version configuration
    
* Automatic switching based on project requirements
    
* Integration with web servers like Apache and Nginx
    
* Management of PHP extensions across versions
    

For now, though, I have a solution that works for me - a machete to cut through the PHP version jungle whenever I need to venture into legacy code territory.

And that client project? I was able to focus on adding the new features they needed without getting bogged down in version compatibility issues. Sometimes, the best solutions are the ones that let you get on with the actual job at hand.

---

*Do you have your own tales of battling PHP version compatibility? Share your experiences in the comments below!*
