Despite widespread adoption, PHP has long received criticism for being inconsistent and encouraging questionable development practices. That said, millions of developers use PHP regularly, and many of them are making real software that solves real business needs, so it's important that we understand how to use the language and deploy it in a modern environment.
Why PHP?
First, let's discuss why developers choose PHP for a project. The language allows programmers to get started quickly, it's well-documented and mature, and there are tons of free resources out there already. Meanwhile, some of the weaknesses that people have pointed out in PHP -- including performance -- have been greatly improved in version 7, and as Keith Adams, Chief Architect at Slack, points out: "PHP gets several things very deeply, and uniquely, right."
Because one of PHP's strengths is its ease of use, I would argue that deploying PHP should be easy as well. In this article, I'm going to offer an overview of the tools and patterns used for setting up modern, scalable, efficient PHP applications on the web. Along the way, I hope to introduce you to some alternatives for each step so you can craft your own unique PHP development environment that has the best balance of simplicity and scalability for your project.
Building a High-Quality PHP Application
Not all PHP applications are created equal. An 18-year-old legacy PHP3 application is likely unusable in a modern server environment, and even applications built using PHP 7 -- the latest version -- aren't all going to be able to take advantage of the tools presented in this article.
If you're starting a new project, read the 12 Factor App and take a look at PHP The Right Way. These sites will point you toward best practices for your application's design, including separating concerns, dependency injection, decoupling your database from your code, and using server environmental variables.
Once your application meets the following requirements, you're ready to get started with the rest of this guide:
The application's database is manageable independent of the application. Usually this requires an ORM like Doctrine or Laravel's Eloquent.
The application loads server and environment-specific variables from a .env file.
Dependencies are managed using Composer.
The application has unit tests (and hopefully integration and acceptance tests too) using PHPUnit.
Changes are tracked via version control. I'm going to assume git for the rest of this guide.
If you're stuck with a legacy PHP application that can't be set up to those requirements, I'd recommend two books: Paul Jones' Modernizing Legacy Applications In PHP and Michael Feathers' Working Effectively with Legacy Code. Both will give you a framework for transforming a messy, spaghetti-strung codebase into a manageable, deployable application.
Setting Up a Local Environment
There are several ways to test and debug a PHP application on your local machine. As mentioned earlier, one of the strengths of PHP is how easy it is to get started, but some methods have distinct advantages.
Option 1: PHP's built-in web server
If you just want to try out a PHP script, the fastest method is to use the built-in web server. In order to use it, you'll need to have the proper version of PHP and any database or caching drivers installed on your machine. However, if the application is simple and you won't be doing extended development on it, this is a perfectly viable option.
Running PHP's built-in server is as simple as running php -S localhost:8000
in the directory that contains your project's index.php
file.
Option 2: Installing globally on your local machine
If you've got a Unix-based operating system, it's pretty easy to set up Apache or NGINX, install PHP, install a database, and then serve a PHP application from a specific folder.
I've successfully used Jason McCreary's guide to set up PHP projects on my Mac in the past. However, the weakness of this method is that the initial setup takes some time, and if you make a major update -- to Apache, MySQL, or PHP -- you have to make that update to all of your PHP projects.
This setup is definitely not ideal if you work with many different projects running on many different environments, but it will give you experience in installing everything directly from the command line.
Option 3: Vagrant & VirtualBox
One of the most popular ways to standardize application environments is through the combination of VirtualBox and Vagrant.
VirtualBox allows you to create virtual machines on your computer, so that you can develop in an environment that is essentially identical to that of your production server (or at least identical to the environment that other devs on your team are using).
Vagrant helps you provision these virtual boxes in a standardized way and pass those configurations around to other team members. There are Vagrant files for just about any version of PHP and combination of web servers and databases imaginable, but if you need a starting point, try out Rasmus Lerdorf's PHP7 Vagrant image.
With this combination of tools, you can seamlessly switch from developing a PHP 5.4 application with MongoDB and NGINX to a PHP 7.0 application using MySQL and Apache in the same day. But there's still a downside: Each virtual machine will take up a significant amount of space, and if you need to run many virtual machines at once, you may quickly reach the limit of your computer's processor or memory.
Option 4: Docker
Docker takes the virtualization of Vagrant and VirtualBox to the next level. It offers improvements in speed and the level of abstraction for sharing environmental setups, and can decrease the time required to get running on new projects. It makes it easier to quickly pass configuration changes around to your team and can be used in conjunction with a continuous integration workflow to run tests faster.
That said, Docker is still a relatively new technology. I've had great experiences running it locally for development, but there is still a valid debate as to whether Docker is ready for production applications. I would say that unless you have a dedicated operations team who can manage Docker in production, it might be best to stick with running it locally first.
Provisioning a production PHP environment
Once you can run and develop code locally and push that code to a repository, you're ready to send it off to a server. If you went with Option 2 in the previous section, then you've already been through the basic process involved in provisioning a server from the operating system up, but that's not the only way to get your PHP application running.
Here are just four of the countless methods you can use to get a server built and ready for your code:
Option 1: Heroku
While Heroku started as a primarily Ruby PAAS solution, they have great support for PHP now, including all the supporting software you need: NGINX, Apache, MySQL, PostgreSQL, Redis, etc. Provisioning a PHP server is as simple as adding a Procfile
and .composer.json
file to your project, but Heroku also has a great walkthrough up on their site.
I find Heroku best for side projects and proofs of concept. While plenty of companies use it at scale, you are limited in how you can configure your server and it can get pricey. The tradeoff between simplicity and flexibility is one you'll have to make based on your needs and available resources.
Option 2: Forge
For PHP deployments, there is nothing quite like Forge. Originally created specifically for Laravel applications, Forge now supports Symfony, WordPress, and with a little work it can be used to host any modern PHP application. I've used it to host an old CodeIgniter 2.2 application, a massive Magento store, and a SlimPHP microservice with ease.
Unlike a PAAS service like Heroku, Forge lets you bring your own server, so you can continue to use AWS, Azure, DigitalOcean, Linode, or just about any virtual host you want (you'll just need sudo access to the machine).
During the setup process, Forge will install all the software you need to run your PHP application and then give you a simple UI to manage standard tasks like updating NGINX configurations, starting queue workers, or changing your deployment scripts. If you ever need to do anything more advanced, you can simply SSH into the server as you would when setting up an environment from scratch.
The only downside to Forge that I've encountered is that it's opinionated, so by default it may not meet your needs or it may install software you don't need. For example, at one of my jobs we hosted databases on one server and code on another. By default, Forge installs MySQL on all your servers, so we had to manually shut it down on the machines that didn't need it.
Option 3: Chef
Most of the work that developers regularly do on a server can be automated, and Chef is a tool that makes it easier to do that. Once installed, you can use it to run the rest of your server setup, and unlike Forge, you get a wide range of customization options. Of course, the downside is that you lose much of the simplicity that tools like Heroku and Forge offer you.
In order to use Chef, you'll need to find or create cookbooks for installing all of your software. This tutorial should help get you started on a basic LAMP stack, but beyond that it will depend on your needs. Chef also requires you to bring your own server, so I'd recommend a virtual machine from your provider of choice.
Option 4: Bash commands/shell scripts
Finally, you can always provision your server from scratch using a virtual (or physical) machine and your terminal. Once the operating system is installed, you'll need to use your OS's package manager to install the following:
Vim (or another text editor) for updating .env and configuration files
PHP compiled with with php-fpm and the database adapter(s) you need
One or more databases (Postgres, MySQL, MariaDB, MongoDB)
Composer for PHP package management
NGINX (or Apache) with FastCGI
Git for pulling code from your repository
Optional but encouraged:
Logging/monitoring software
Caching service (memcache, redis)
I won't go through the steps of installing all that here because it's very involved, and unless you have a unique environmental requirement or you're trying to look busy for your boss, it's probably not a good idea to do all this manually.
The problem is that you'll have to do this again anytime you want to add a new server. This is incredibly cumbersome, so while some shell scripts could certainly help, keeping those up to date as requirements and software changes is a job in itself.
Continuous Integration and Deployment
Once you have a server provisioned, you'll just need to pull the latest code from your repository and point your web server to the directory. If everything is set up properly and the application is configured, you should have a running web application. But you're not really done yet.
The last step in creating a modern PHP application is ensuring that our code is tested (continuous integration) and deployed (continuous deployment) every time updates are made. Continuous integration and deployment is one of the most powerful tools in modern application development because it allows developers to confidently make changes to their code and users to benefit from updates, new features, and bug fixes faster.
Enter Codeship
While you could set up your own continuous integration server, it's a ton of work compared with using a continuous integration service like Codeship. Codeship will install your application's dependencies and run your test suite every time code is updated in your repository. If the branch being built has a deployment pipeline, it will trigger your deployment method of choice. You also get an alert any time a build fails, and you can see where in the process things went wrong.
How you use Codeship to trigger a deployment will depend on how you set up the server (see the section "Provisioning a production PHP environment" above):
If you're using Heroku: Use the built-in Codeship-Heroku integration. This just requires your application name and Heroku API key.
If you're using Forge: Forge automatically creates a "Deployment Trigger URL" with each application you deploy. Use Codeship's "Custom Script" option to trigger a
curl
request to this webhook, and your Forge deployment script will automatically be run when Codeship successfully builds your code.If you're using Chef or bash scripts: You'll need to write your own custom shell script, create a webhook on your server, or use scp to get code deployed. This is more work, but again, it allows for the greatest customization in deploying your code.
Conclusion
The options outlined above are not meant to be comprehensive, but they serve as a starting point for setting up and deploying a modern PHP application. There are a number of factors in building scalable PHP applications that I have not addressed, including caching, scaling horizontally, and tweaking PHP-FPM and NGINX configurations. The tools and methods you use should depend on your project's requirements, the size of your team, and the amount of time and money you have for setting up your application.
Web application development is a constantly changing field, and our best practices today may be outdated in a few months. If you have comments or think there is anything I missed, I'd love to hear from you. Leave a comment below or find me on Twitter.