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.
Luckily for us, we can rely on tools like Ansible or Docker to provision our infrastructure.
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 SetEnv
directive:
<VirtualHost *:80>
ServerName myapp.com
ServerAdmin me@myapp.com
DocumentRoot /app/htdocs
<Directory /app/htdocs>
# […]
</Directory>
# […]
# Set Env vars, if any
SetEnv DATABASE_URI "mysql://foo:bar@myserver/mydatabase"
# […]
</VirtualHost>
On Nginx, we can use the fastcgi_param
directive:
server {
# […]
location ~ \.php$ {
# […]
fastcgi_param DATABASE_URI mysql://foo:bar@myserver/mydatabase;
}
}
In development we can use a .env
file, which is not tracked by version control, to store these setting easily:
APP_MODE=development
DATABASE_URI=mysql://foo:bar@myserver/mydatabase
# […]
Our application will use a library like phpdotenv to access these settings:
<?php
// path/to/init.php
require __DIR__ . '/path/to/vendor/autoload.php';
// Load Environment
$envFile = dirname(__FILE__) . '/path/to/.env';
if (is_readable($envFile)) {
$dotenv = new Dotenv\Dotenv(dirname($envFile));
$dotenv->load();
}
After the init phase we can access database credentials stored in $_SERVER['DATABASE_URI']
.
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.
Among PHP-specific tools we have Phing, Deployer and Rocketeer. We can also use non-PHP-specific tools like Capistrano or Ansible.
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)
Moreover, we can take advantage of Continuous Integration platforms like TravisCI and CodeShip and make them trigger a deploy whenever we update a specific repository branch.
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.
We can avoid this pitfall by using Memcached or Redis as session handlers:
<?php
// For Memcache
// $_SERVER['SESSION_HANDLER'] = 'memcache'
// $_SERVER['SESSION_PATH'] = 'tcp://your-memcache-host:11211?persistent=1&weight=2&timeout=2&retry_interval=10'
// For Redis
// $_SERVER['SESSION_HANDLER'] = 'redis'
// $_SERVER['SESSION_PATH'] = 'tcp://your-redis-host:6379?auth=yourverycomplexpasswordhere'
ini_set('session.save_handler', $_SERVER['SESSION_HANDLER']);
ini_set('session.save_path', $_SERVER['SESSION_PATH']);
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.
There are some alternative solutions like Appserver and ReactPHP, very interesting but they are not very much widespread.
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
There are no PHP specific things here, thanks to Vagrant and Docker we can setup our development boxes to be nearly identical to production ones.
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
Having admin processes is not a problem, as long as our app is well designed. If we need a REPL shell we can use psysh. I would also recommend Phinx for database migrations.
Conclusions
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.