My last week's blog post on running Apache, MySQL, PHP server on Windows with multiple, simultaneous PHP versions seems to have been a smash hit. This week we'll be doing the same thing on Mac OS X. For those of you who didn't click the link, I decided it would be a cool, geeky project to implement an Apache-MySQL-PHP web server without using a pre-packaged server like MAMP or Zend Server. My goal was to have the same sites run under different versions of PHP by just visiting a different URL on my browser. This makes cross-PHP testing of sites a piece of cake.

Until now I was using Zend Server as my primary development environment. Since the last two major versions were becoming a bit of a resource hog. The much touted Z-Ray never really worked for me. That sealed its fate. I had to replace it with something nimbler and more convenient. I also wanted to no longer have to use MAMP to test my software across different PHP versions. I certainly did not want to have to go through the stop servers - change PHP version - start servers routine ever again. The process on Mac OS X is much more involving that what I had done on Windows last week but I was determined to succeed. And yes, I did succeed!

Disclaimer: the configuration I am proposing is only suitable for a local development server. Do try this at home and only at home – or at least not in a production server. The instructions below worked for me on Mac OS X Mavericks but should work on Lion and Mountain Lion. At the time of this writing OS X Yosemite is not released yet so your mileage may vary (read: serious breakage may occur). Another major pitfall is that the PHP binaries I am using are only supported on 64-bit Macs. If you have an old machine with an Intel Core Duo you're out of luck.

Warning: Pretty much all of the instructions have to be carried out using the command line. You can use Terminal (easy way to get there: type ⌘Space, type Terminal and hit ENTER) or another terminal emulator application such as iTerm2. If you don't feel comfortable using the command line stop reading now and download MAMP.

Setting up MySQL

Go to MySQL's download page and download the appropriate DMG package. For Mac OS X Lion and above it's the "Mac OS X 10.8 (x86, 64-bit), DMG Archive". For detailed installation instructions read MySQL's documentation page on Mac OS X installation. MySQL is installed to /usr/local/mysql

After installing MySQL, edit the file ~/.bash_profile Easy way to do it from the command line

 open ~/.bash_profile

Add this line at the bottom of the file

export PATH=/usr/local/mysql/bin:$PATH

Close the terminal and open it again. Verify that you can access MySQL command line (note that it's a capital V in the command)

mysql -V

You should see something like

mysql  Ver 14.14 Distrib 5.6.20, for osx10.8 (x86_64) using  EditLine wrapper

Good to know: Even though MySQL auto-starts on boot, you can still start and stop MySQL from the Terminal using the commands

sudo /Library/StartupItems/MySQLCOM/MySQLCOM start
sudo /Library/StartupItems/MySQLCOM/MySQLCOM stop

Pro tip: I recommend using Sequel Pro to manage your MySQL databases instead of phpMyAdmin. It's much faster.

Setting up PHP

We will be using the pre-built binaries offered at liip.ch. It's one of the binary distributions recommended on php.net and by far the easiest to install.

Install all versions of PHP you want to use. I chose all four of them. Please note that due to PHP 5.3 missing the FastCGI binary you must install it last.

curl -s http://php-osx.liip.ch/install.sh | bash -s 5.6
curl -s http://php-osx.liip.ch/install.sh | bash -s 5.5
curl -s http://php-osx.liip.ch/install.sh | bash -s 5.4
curl -s http://php-osx.liip.ch/install.sh | bash -s 5.3

Edit the file ~/.bash_profile The easy way to do it from the command line is typing

 open ~/.bash_profile

Add these lines at the bottom of the file

export PHPRC=/usr/local/php5-5.4.32-20140822-093224/lib
export PHP_INI_SCAN_DIR=/usr/local/php5-5.4.32-20140822-093224/php.d
export PATH=/usr/local/php5-5.4.32-20140822-093224/bin:$PATH

Note that the php5-5.4.32-20140822-093224 part is most likely different on your machine. You can find out which directory to use by typing in the Terminal

ls /usr/local/ | grep php5-5.4

If you want to use a version different than PHP 5.4 as your default command line PHP executable just substitute 5.4 with 5.3, 5.5 or 5.6 in the command above.

Close the terminal application and open it again. Verify that you can access PHP command line

php -v

You should see something like

PHP 5.4.32 (cli) (built: Aug 22 2014 09:28:20) 
Copyright (c) 1997-2014 The PHP Group
Zend Engine v2.4.0, Copyright (c) 1998-2014 Zend Technologies
    with Xdebug v2.2.3, Copyright (c) 2002-2013, by Derick Rethans

If you get an error you need to run the following commands in the Terminal

sudo su
for d in `find /usr/local -type d -name "php5-*" -depth 1`; do \
for x in libtidy-0.99.0.dylib libintl.8.dylib libpng12.0.dylib libjpeg.7.dylib libxml2.2.dylib libfreetype.6.dylib; \
do install_name_tool -change /usr/local/php5/lib/$x $d/lib/$x $d/bin/php; \
done; \
done
for d in `find /usr/local -type d -name "php5-*" -depth 1`; do \
for l in `find $d/lib/php/extensions -type f -name "*.so"`; do \
for x in libcurl.4.dylib libintl.8.dylib libidn.11.dylib libgmp.10.dylib libicui18n.46.dylib libicuuc.46.dylib \
libicudata.46.dylib libicuio.46.dylib libmcrypt.4.dylib libmemcached.11.dylib libmemcachedutil.2.dylib \
libsybdb.5.dylib libcurl.4.dylib libidn.11.dylib libintl.8.dylib libsybdb.5.dylib libpq.5.dylib \
libpq.5.dylib libssh2.1.dylib libexslt.0.dylib libxslt.1.dylib libxml2.2.dylib; \
do install_name_tool -change /usr/local/php5/lib/$x $d/lib/$x $l; \
done; \
done; \
done

These commands will relink the PHP binaries and extensions to use the correct directories for our multi-PHP server.

At this point all PHP versions try to load the same PHP modules from the PHP 5.4 directory. This won't work. Luckily, it's an easy fix with some command line kung-fu. In your trusted Terminal enter:

sudo su
for d in `find /usr/local -type d -name "php5-*" -depth 1`; do \
pushd $d; massreplace='s/\/usr\/local\/php5\//\/usr\/local\/'${PWD##*/}'\//g'; sed -i .bak $massreplace $d/php.d/*.ini; popd; \
done

Setting up Apache

We will be using Apache 2.2 which is helpfully shipped with Max OS X. Unlike previous versions of Mac OS X there is no longer a GUI to activate it, so we'll have to do everything old school, using our trusted command line. That's the beauty of Mac OS X: no matter how fancy the interface grows, it's still a pretty standard BSD system under the hood. Being BSD we can go old school with it any time we want. That's exactly what makes a geek rejoice!

Enabling FastCGI support

We're using Apache 2.2 shipped with Mac OS X, but it doesn't have built-in support for FastCGI and we need it to support multiple PHP versions. No problem, we can work around that by using MacPorts to get the missing bits.

If you haven't done so already, install MacPorts. I had it installed since a long time ago so please forgive me if there's a missing step here.

Close and reopen the Terminal application. Now type

sudo port selfupdate
sudo port install mod_fcgid

After mod_fcgid is downloaded (it takes several minutes to install all of the dependencies) we need to move it to somewhere Apache can find it.

sudo cp /opt/local/apache2/modules/mod_fcgid.so /usr/libexec/apache2

Edit the Apache configuration file /etc/apache2/httpd.conf You can only do that through the command line due to the advanced security model of Mac OS X Lion and later. I know, you can't use your mouse. You have to go really old school, using the keyboard. nano helpfully displays a list of common commands at the bottom rows. Press ⌃g to get the full list of keyboard commands. Remember that ^ stands for the ctrl (⌃) key and M stands for the alt (⌥) key. Therefore ^ k means hold down the ctrl key and press the K key, M a means hold down the alt key and press the A key and M A means hold down the alt and shift keys and press the A key. Congrats, you had your crash course in UNIX file editing. OK, back to the command to edit the file:

sudo nano /etc/apache2/httpd.conf

Find all the LoadModule lines. After the last one add

LoadModule fcgid_module libexec/apache2/mod_fcgid.so

Save and close the file by pressing ⌃x, then Y, then ENTER.

Now restart Apache

sudo apachectl -k restart

If you screwed up, Apache will complain. If you get no feedback after running this command it worked just fine. In true UNIX tradition you get a message only if something's wrong.

Auto-start Apache on boot

From the Terminal

sudo launchctl load -w /System/Library/LaunchDaemons/org.apache.httpd.plist

Changing the user and group of Apache

Normally Apache runs as user www and group www. However, if you are developing locally you may want to sacrifice security over convenience. After all, the web server only serves local requests (Mac OS X firewall blocks access to Apache by default). Edit the Apache configuration file /etc/apache2/httpd.conf like before

sudo nano /etc/apache2/httpd.conf

Find these lines

User _www
Group _www

and change them to

User myuser
Group staff

where myuser is your username.

If you don't know what your username is, from another Terminal give the command

whoami

and Mac OS X will tell reply with your username.

Save and close the file by pressing ⌃x, then Y, then ENTER.

Finally, restart Apache

sudo apachectl -k restart

Configuring PHP

First we need to know which directories PHP 5.3, 5.4, 5.5 and 5.6 is installed in. From the terminal window do

ls /usr/local/ | grep php5

You should see something like

php5
php5-5.3.29-20140818-160244
php5-5.4.32-20140822-093224
php5-5.5.16-20140822-215341
php5-5.6.0RC4-20140818-155418

Note down these directory names. You will need to replace them in the instructions below.

Let's create a new web root inside your home directory

mkdir ~/Sites
cd ~/Sites
pwd

The final command will reply with something like

/Users/myuser/Sites

Copy that path. It's the absolute path to your web root and you will need it below.

Edit the Apache configuration file /etc/apache2/httpd.conf like before:

sudo nano /etc/apache2/httpd.conf

Find the line

Include /private/etc/apache2/other/*.conf

It should be the last line. Put a hash sign in front of it

#Include /private/etc/apache2/other/*.conf

This removes the single PHP version configuration which was added by the PHP installation step.

While we are at it, let's change the web root to the new directory we created before. Find this line

DocumentRoot "/Library/WebServer/Documents"

and change it to

DocumentRoot "/Users/myuser/Sites"

where /Users/myuser/Sites is the absolute path to your web root you previously noted down.

Important! Please remember to change all other instances of /Library/WebServer/Documents to /Users/myuser/Sites inside this file, especially the Directory open tag.

Next, find the line

#Include /private/etc/apache2/extra/httpd-default.conf

and remove the hash sign in front

Include /private/etc/apache2/extra/httpd-default.conf

Save and close the file by pressing ⌃x, then Y, then ENTER.

Now let's edit the httpd-default.conf Again, due to Mac OS X security model we need to do this from the command line

sudo nano /etc/apache2/extra/httpd-default.conf 

Append these lines to the file

FcgidInitialEnv PATH "/usr/local/php5:/usr/sbin:/usr/local/bin:/usr/bin"
FcgidInitialEnv TEMP "/tmp"
FcgidInitialEnv TMP "/tmp"
FcgidIOTimeout 64
FcgidConnectTimeout 16
FcgidMaxRequestsPerProcess 1000
FcgidMaxProcesses 50
FcgidMaxRequestLen 8131072

# Location of php.ini FcgidInitialEnv PHPRC "/usr/local/php5/lib" FcgidInitialEnv PHP_INI_SCAN_DIR "/usr/local/php5/php.d" FcgidInitialEnv PHP_FCGI_MAX_REQUESTS 1000

Save and close the file by pressing CTRL-x, then Y, then ENTER.

Now restart Apache

sudo apachectl -k restart

Test PHP

Create a new file named phpinfo.php inside the Sites directory under your home directory with the content

<?php phpinfo(); ?>

On your browser visit http://localhost/phpinfo.php You should see the PHP information page. If you have got all the instructions right you should see that right below the top line, showing the version of PHP, there is this line

php-osx.liip.ch by Liip (originally developed by www.local.ch)

If not, review all the instructions above, you missed a step.

The Virtual Hosts

Our goal is to have every site on our local server run under a different PHP version depending on the domain name we are using. The domain names we will be using are:

* local.web for PHP 5.4
* local53.web for PHP 5.3
* local55.web for PHP 5.5
* local56.web for PHP 5.6

You need to change your hosts file to have Mac OS X map these fake domains to your local server. The easiest way is using Gas Mask. Add these lines to the hosts file:

127.0.0.1 local.web www.local.web
127.0.0.1 local53.web www.local53.web
127.0.0.1 local55.web www.local55.web
127.0.0.1 local56.web www.local56.web

We will also be implementing dynamic virtual hosts. This means that when you have a directory ~/Sites/foobar you will be able to access this site both as http://local.web/foobar and http://foobar.local.web The latter is a better way to develop sites locally since most web scripts store image paths relative to the domain root. For this trick to work you’d need to add a few more lines to your hosts file:

127.0.0.1 foobar.local.web
127.0.0.1 foobar.local53.web
127.0.0.1 foobar.local55.web
127.0.0.1 foobar.local56.web

Now the site in the directory ~/Sites/foobar will be accessible as http://foobar.local.web (PHP 5.4), http://foobar.local53.web (PHP 5.3), http://foobar.local55.web (PHP 5.5) and http://foobar.local56.web (PHP 5.6). Cool! Let’s make the changes in our server’s configuration to make that happen.

Configure Apache for multiple PHP versions

Edit the /etc/apache2/httpd.conf. As we have already said, we have to use the Terminal

sudo nano /etc/apache2/httpd.conf

Find the line

# Include conf/extra/httpd-vhosts.conf

and remove the trailing hash sign so that it becomes

Include conf/extra/httpd-vhosts.conf

Save and close the file by pressing ⌃x, then Y, then ENTER.

Now edit the /etc/apache2/extra/httpd-vhosts.conf file with

sudo nano /etc/apache2/extra/httpd-vhosts.conf

and change its contents with:

NameVirtualHost *:80
# Default PHP virtual host (local.web) <VirtualHost *:80> ServerAdmin This email address is being protected from spambots. You need JavaScript enabled to view it. DocumentRoot "/Users/myuser/Sites" ServerName www.local.web FcgidInitialEnv PATH "/usr/local/php5-5.4.32-20140822-093224:/usr/sbin:/usr/local/bin:/usr/bin" FcgidInitialEnv PHPRC "/usr/local/php5-5.4.32-20140822-093224/lib" FcgidInitialEnv PHP_INI_SCAN_DIR "/usr/local/php5-5.4.32-20140822-093224/php.d" <Directory "/Users/myuser/Sites"> DirectoryIndex index.html index.php <Files ~ "\.php$"> AddHandler fcgid-script .php FcgidWrapper "/usr/local/php5-5.4.32-20140822-093224/bin/php-cgi" .php Options +ExecCGI order allow,deny allow from all deny from none </Files> </Directory> </VirtualHost>
# Dynamic virtual hosts using vhost_alias, default PHP <VirtualHost *:80> ServerAlias *.local.web UseCanonicalName Off VirtualDocumentRoot "/Users/myuser/Sites/%1" FcgidInitialEnv PATH "/usr/local/php5-5.4.32-20140822-093224:/usr/sbin:/usr/local/bin:/usr/bin" FcgidInitialEnv PHPRC "/usr/local/php5-5.4.32-20140822-093224/lib" FcgidInitialEnv PHP_INI_SCAN_DIR "/usr/local/php5-5.4.32-20140822-093224/php.d" <Directory "/Users/myuser/Sites"> DirectoryIndex index.html index.php <Files ~ "\.php$"> AddHandler fcgid-script .php FcgidWrapper "/usr/local/php5-5.4.32-20140822-093224/bin/php-cgi" .php Options +ExecCGI order allow,deny allow from all deny from none </Files> </Directory> </VirtualHost>
# PHP 5.5 virtual host (local55.web) <VirtualHost *:80> ServerAdmin This email address is being protected from spambots. You need JavaScript enabled to view it. DocumentRoot "/Users/myuser/Sites" ServerName www.local55.web FcgidInitialEnv PATH "/usr/local/php5-5.5.16-20140822-215341:/usr/sbin:/usr/local/bin:/usr/bin" FcgidInitialEnv PHPRC "/usr/local/php5-5.5.16-20140822-215341/lib" FcgidInitialEnv PHP_INI_SCAN_DIR "/usr/local/php5-5.5.16-20140822-215341/php.d" <Directory "/Users/myuser/Sites"> DirectoryIndex index.html index.php <Files ~ "\.php$"> AddHandler fcgid-script .php FcgidWrapper "/usr/local/php5-5.5.16-20140822-215341/bin/php-cgi" .php Options +ExecCGI order allow,deny allow from all deny from none </Files> </Directory> </VirtualHost>
# Dynamic virtual hosts using vhost_alias, PHP 5.5 <VirtualHost *:80> ServerAlias *.local55.web UseCanonicalName Off VirtualDocumentRoot "/Users/myuser/Sites/%1" FcgidInitialEnv PATH "/usr/local/php5-5.5.16-20140822-215341:/usr/sbin:/usr/local/bin:/usr/bin" FcgidInitialEnv PHPRC "/usr/local/php5-5.5.16-20140822-215341/lib" FcgidInitialEnv PHP_INI_SCAN_DIR "/usr/local/php5-5.5.16-20140822-215341/php.d" <Directory "/Users/myuser/Sites"> DirectoryIndex index.html index.php <Files ~ "\.php$"> AddHandler fcgid-script .php FcgidWrapper "/usr/local/php5-5.5.16-20140822-215341/bin/php-cgi" .php Options +ExecCGI order allow,deny allow from all deny from none </Files> </Directory> </VirtualHost>
# PHP 5.6 virtual host (local56.web) <VirtualHost *:80> ServerAdmin This email address is being protected from spambots. You need JavaScript enabled to view it. DocumentRoot "/Users/myuser/Sites" ServerName www.local56.web FcgidInitialEnv PATH "/usr/local/php5-5.6.0RC4-20140818-155418:/usr/sbin:/usr/local/bin:/usr/bin" FcgidInitialEnv PHPRC "/usr/local/php5-5.6.0RC4-20140818-155418/lib" FcgidInitialEnv PHP_INI_SCAN_DIR "/usr/local/php5-5.6.0RC4-20140818-155418/php.d" <Directory "/Users/myuser/Sites"> DirectoryIndex index.html index.php <Files ~ "\.php$"> AddHandler fcgid-script .php FcgidWrapper "/usr/local/php5-5.6.0RC4-20140818-155418/bin/php-cgi" .php Options +ExecCGI order allow,deny allow from all deny from none </Files> </Directory> </VirtualHost>
# Dynamic virtual hosts using vhost_alias, PHP 5.6 <VirtualHost *:80> ServerAlias *.local56.web UseCanonicalName Off VirtualDocumentRoot "/Users/myuser/Sites/%1" FcgidInitialEnv PATH "/usr/local/php5-5.6.0RC4-20140818-155418:/usr/sbin:/usr/local/bin:/usr/bin" FcgidInitialEnv PHPRC "/usr/local/php5-5.6.0RC4-20140818-155418/lib" FcgidInitialEnv PHP_INI_SCAN_DIR "/usr/local/php5-5.6.0RC4-20140818-155418/php.d" <Directory "/Users/myuser/Sites"> DirectoryIndex index.html index.php <Files ~ "\.php$"> AddHandler fcgid-script .php FcgidWrapper "/usr/local/php5-5.6.0RC4-20140818-155418/bin/php-cgi" .php Options +ExecCGI order allow,deny allow from all deny from none </Files> </Directory> </VirtualHost>
# PHP 5.3 virtual host (local53.web). Note that PHP 5.3 uses mod_php instead of FastCGI. LoadModule php5_module /usr/local/php5-5.3.29-20140818-160244/libphp5.so PHPINIDir "/usr/local/php5-5.3.29-20140818-160244/lib"
<VirtualHost *:80> ServerAdmin This email address is being protected from spambots. You need JavaScript enabled to view it. DocumentRoot "/Users/myuser/Sites" ServerName www.local53.web SetEnv PHP_INI_SCAN_DIR "/usr/local/php5-5.3.29-20140818-160244/php.d" <Directory "/Users/myuser/Sites"> DirectoryIndex index.html index.php AddType application/x-httpd-php .php AddType application/x-httpd-php-source .phps <Files ~ "\.php$"> Options +ExecCGI order allow,deny allow from all deny from none </Files> </Directory> </VirtualHost>
# Dynamic virtual hosts using vhost_alias, PHP 5.3 <VirtualHost *:80> ServerAlias *.local53.web UseCanonicalName Off VirtualDocumentRoot "/Users/myuser/Sites/%1" SetEnv PHP_INI_SCAN_DIR "/usr/local/php5-5.3.29-20140818-160244/php.d" <Directory "/Users/myuser/Sites"> DirectoryIndex index.html index.php AddType application/x-httpd-php .php AddType application/x-httpd-php-source .phps <Files ~ "\.php$"> Options +ExecCGI order allow,deny allow from all deny from none </Files> </Directory> </VirtualHost>

Please remember to change /Users/myuser/Sites with the path to your web root. You will most likely need to change the paths to each individual PHP version, e.g. php5-5.6.0RC4-20140818-155418 with whatever is the directory name of your PHP 5.6 version.

Save and close the file by pressing ⌃x, then Y, then ENTER.

Now, let's restart Apache one final time

sudo apachectl -k restart

Test the solution

We can now use four different domains to access the same content as different PHP versions. Let’s try it:

http://www.local.web/phpinfo.php reports PHP 5.4

http://www.local53.web/phpinfo.php reports PHP 5.3

http://www.local55.web/phpinfo.php reports PHP 5.5

http://www.local56.web/phpinfo.php reports PHP 5.6

All three URLs load the same file /Users/myuser/Sites/phpinfo.php.

Outro

At some point you'll probably need to change the PHP configuration. Inside each one of the /usr/local/php5-SOMETHING directories there's the php.d/99-liip-developer.ini file. This is where you are supposed to add your custom configuration variables. Since this file loads last it overrides the configuration in all other files. Also remember that each PHP version has its own INI file. Moreover, since PHP 5.3 loads as an Apache module it requires you to restart Apache before the changes have any effect.

If you are looking for the server access and error log files they can be found at the /var/log/apache2 directory. If you would rather remove your eye from its socket with a rusty fork instead of using the terminal to read the logs, I feel for you and have to propose a solution: just install Pimp My Log. It will make your life decidedly easier.

If you are trying to score extra geek points and make your PHP development process a tad easier you might also be interested in PHP Error, especially its "I want it outside my project, but running on all dev sites" configuration. Since you're running all PHP versions, except 5.3, as FastCGI you can put the PHP configuration value php_error.autorun = On to enable it or php_error.force_disabled = On to disable it inside your .htaccess file.

Finally, remember that you can always install this custom server alongside MAMP as long as MAMP is set to use its custom ports (8888 for the web server and 8889 for MySQL).

Good luck and have fun!