The twelve-factor app is a methodology for building modern software-as-a-service applications that take advantage of cloud platforms. At first glance, languages such as Ruby, Node.js and Python seem more suited than PHP to this concept. Nevertheless, PHP can be “12factorizeable” without too much effort with a clever application design.
Let’s see those factors one by one and see how we can use them in practice today.
1 - Codebase
One codebase tracked in revision control, many deploys
We have no particular problems here, it’s just a matter of development workflow. We can track our app code in Git: the master branch is deployed to production, while all the development and testing is made under separate branches.
2 - Dependencies
Explicitly declare and isolate dependencies
We got this too, we have Composer for PHP dependencies and we could add NPM or Bower for the front end. Ideally, we should not rely on the implicit existence of any system tools, such as
curl or ImageMagick, but even our Ruby and Python cousins can have some problems on this.
3 - Config
Store config in the environment
We can have this, but we have some organisational work to do. First, we need to divide settings that do not vary between deploys: these can be safely stored in configuration files and added to the app repository.
Then we need to collect other settings that vary, such as credentials, database connections and so on, and store them in the environment. But how?
On Apache, we can set the environment inside our virtual host file with the
On Nginx, we can use the
In development we can use a
.env file, which is not tracked by version control, to store these setting easily:
Our application will use a library like phpdotenv to access these settings:
After the init phase we can access database credentials stored in
4 - Backing services
Treat backing services as attached resources
A backing service is any service the app consumes over the network as part of its normal operation.
Here we’re talking about databases (MySQL/NoSQL), queue services, SMTP servers and caching systems. In some cases, even the filesystem should be considered a backing service, for example when running on Heroku, where the local filesystem is reset at each deployment.
Just like factor #3, this is more a design task than a technical limit. We need to design our application in a way that both local and remote resources are treated equal and can be swappable.
Unfortunately, there is not a one-size-fits-all solution.
An easy case could be the database connection. If we use an ORM library (or PDO directly), with connection strings in the environment, we can freely use
mysql://localhost/devdb in development and
mysql://produser:prodpass@prodserver/proddb in production.
Other services can be more or less complex to manage, but the principle is the same: we use
parse_url() to get the pieces and feed them to an adapter object.
Filesystem services are a bit hard on this. There is a good abstraction library called Flysystem that comes equipped with adapters for local filesystems, Amazon S3, Dropbox, FTP and many others.
5 - Build, release, run
Strictly separate build and run stages
In PHP the run stage is usually delegated to an Apache or Nginx server that may need to be restarted or reloaded on each deploy. For the build and release stages, we can count on a good list of available tools.
With Ansible we can create a playbook that tells our destination server(s) to:
- clone/update the codebase to a specific version (build #1)
- run Composer scripts (build #2)
- run database migrations scripts (release #1)
- update configuration on the environment if needed (release #2)
- flush caches (run #1)
- restart the services (run #2)
6 - Processes
Execute the app as one or more stateless processes
PHP processes are already stateless and shared-nothing, although sometimes we tend to use the built-in file storage for sessions, and this is not advisable on a cloud platform.
All other data should be stored in a database. The local filesystem can still be useful as a disposable cache for things like compiled assets or templates, if not already compiled during the build stage.
7 - Port binding
Export services via port binding
The twelve-factor app is completely self-contained and does not rely on runtime injection of a web server into the execution environment to create a web-facing service. The web app exports HTTP as a service by binding to a port and listening to requests coming in on that port.
Well, this is a bit tricky because PHP has been designed to use a web server. Some providers, like Heroku, address the issue by embedding the web server into the application as a dependency, through their PHP Buildpack that runs Apache/Nginx/HHVM in foreground mode.
8 - Concurrency
Scale out via the process model
Similar to factor #7, the dependency from a web server makes PHP different. However, every request/response is handled by its own process so we can safely assume that PHP uses the process model.
9 - Disposability
Maximize robustness with fast startup and graceful shutdown
I’m not an authority here, but I trust people smarter than me when they say that PHP already works this way.
10 - Dev/prod parity
Keep development, staging, and production as similar as possible
11 - Logs
Treat logs as event streams
Logging to a stream has never been a problem for PHP, and with Monolog it’s even easier. The execution environment will take care to process the streams, so this is not our problem… unless we’re also the DevOps guys.
12 - Admin processes
Run admin/management tasks as one-off processes
We’ve addressed all the original 12 factors. And despite a few design differences between PHP and its “competitor” languages, we can safely use it to build robust cloud-native applications.
So, where to go from here? First: start coding now! Then read Beyond the Twelve-Factor App by Kevin Hoffman, which expands the original guidelines to help you even better applications.