Deploying PHP applications with Phing
How many steps are required to deploy your software? Some people say it shouldn’t be more than one. I’m little bit more relaxed about it so I would say two steps are still fine. If it takes more than two then most likely you need a build script.
Why do you need a build script? Because your time is precious. You don’t want to waste it on repetitive boring tasks. You don’t want to waste other people’s time either. If you forget to tell your collegues that they have to do something after code update (like release a database patch) they might get confused. It’s also not safe. More things to do during a deployment means higher chance something will go wrong. Human is the weakest link in any system.
Every deployment process is unique to an application but there are few common areas:
- Fetching latest code
- Configuration
- File system operations
- Database update
There might be more areas but those are the most popular. Each of them can require multiple actions. All of that and much more can be handled though a build tool.
Phing is a PHP project build system or build tool based on Apache Ant. You can do anything with it that you could do with a traditional build system like GNU make. It use simple XML build files and extensible PHP “task” classes.
There are many ways to install Phing. My recommendation is to go for the composer. After all we want to limit number of deployment steps.
Create or edit composer configuration file.
1 |
$ vim composer.json |
Add phing to the “require” section.
1 2 3 4 5 |
{ "require": { "phing/phing": "2.5.0" } } |
Install Phing.
1 |
$ php composer.phar install |
After Phing is installed you can access it directly from vendors directory.
1 |
$ vendor/phing/phing/bin/phing |
If you don’t like the long path you can always create a symlink in “/usr/local/sbin” or any other preferred location.
The build script we are going to create will do three things: generate configuration, create logs file and release database changes. Each of those targets will have own build file. You can obviously keep everything in one script but it’s XML. Even short scripts can easily get unreadable.
First lets create the main build.xml file. It will bound all XML files together.
1 |
$ vim build.xml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<?xml version="1.0" encoding="UTF-8"?> <project name="MyApplication" default="main" basedir="."> <property file="./build/properties/default.properties" /> <if> <isset property="build.env" /> <then> <echo message="Overwriting default.properties with ${build.env}.properties" /> <property file="./build/properties/${build.env}.properties" override="true" /> </then> </if> <target name="main"> <echo message="+------------------------------------------+"/> <echo message="| |"/> <echo message="| Building The Project |"/> <echo message="| |"/> <echo message="+------------------------------------------+"/> <phing phingfile="${project.basedir}/build/build-logs.xml" target="logs" /> <phing phingfile="${project.basedir}/build/build-configuration.xml" target="configuration" /> <phing phingfile="${project.basedir}/build/build-database.xml" target="database" /> </target> <target name="database-init"> <phing phingfile="${project.basedir}/build/build-database.xml" target="database-init" /> </target> </project> |
The script defines two targets: main and database-init. The default target defined in the “project” tag is “main”. If you run Phing without any parameters this is the target which is going to be executed.
The default target doesn’t do anything on it’s own. It calls three other targets: logs, configuration and database from external files. We will create those files in a second.
Build.xml does one very important thing. It loads properties.
1 2 |
$ mkdir -p build/properties/ $ vim build/properties/default.properties |
1 2 3 4 5 6 7 |
db.host = 127.0.0.1 db.user = root db.password = root db.database = foobar facebook.appId = 1234567890-10 facebook.secret = 1234567890qwertyuiopasdfghjkl |
A property file has similar syntax to an ini file. Defined values will be used to build the application. Every environment (for example: production, staging, workdev, homedev, etc) will most likely have own property file. Duplicating everything for a new environment is not the best approach. Imagine you create a new setting which will be the same for all environments. You will have to paste it to every property file. To avoid this scenario my proposition is to have a default file which is always loaded on startup. Settings unique to an environment should overwrite the default one.
1 |
$ vim build/properties/production.properties |
1 2 |
db.user = "usr01" db.password = "s3cr3t" |
Now if you deploy to production set “build.env” property accordingly.
1 |
$ phing -Dbuild.env=production |
This is simple but powerful strategy.
Lets create now missing XML files.
1 |
$ vim build/build-logs.xml |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?xml version="1.0" encoding="UTF-8"?> <project default="logs"> <target name="logs"> <echo msg="Creating logs directory..." /> <mkdir dir="${project.basedir}/logs" /> <echo msg="Creating application.log file..." /> <touch file="${project.basedir}/logs/application.log" /> <chown file="${project.basedir}/logs/application.log" user="www-data.www-data" verbose="true" /> </target> </project> |
1 |
$ vim build/build-configuration.xml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?xml version="1.0" encoding="UTF-8"?> <project default="configuration"> <target name="configuration"> <echo msg="Building configuration..." /> <mkdir dir="${project.basedir}/conf" /> <copy todir="${project.basedir}/conf/" overwrite="true"> <filelist dir="${project.basedir}/build/templates/" files="application.ini.build" /> <mapper type="regexp" from="^(.*).build$" to="\1"/> <filterchain> <expandproperties /> </filterchain> </copy> </target> </project> |
1 |
$ vim build/build-database.xml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<?xml version="1.0" encoding="UTF-8"?> <project default="database"> <target name="database"> <echo msg="Deploying database..." /> <tstamp/> <property name="build.dbdeploy.deployfile" value="${project.basedir}/database/deploy-db-${DSTAMP}${TSTAMP}.sql" /> <property name="build.dbdeploy.undofile" value="${project.basedir}/database/undo-db-${DSTAMP}${TSTAMP}.sql" /> <echo message="Database undo file ${build.dbdeploy.undofile}" /> <dbdeploy url="mysql:host=${db.host};dbname=${db.database}" userid="${db.user}" password="${db.password}" dir="${project.basedir}/database/delta/" outputfile="${build.dbdeploy.deployfile}" undooutputfile="${build.dbdeploy.undofile}" /> <exec command="mysql -h${db.host} -u${db.user} -p${db.password} ${db.database} < ${build.dbdeploy.deployfile}" dir="${project.basedir}" checkreturn="true" /> </target> <target name="database-init"> <echo msg="Initialising database..." /> <exec command="mysql -h${db.host} -u${db.user} -p${db.password} ${db.database} < ${project.basedir}/build/templates/database.sql" dir="${project.basedir}" checkreturn="true" /> </target> </project> |
The easiest target is “logs“. It creates logs directory with application.log file. It also changes ownership of that file to the Apache user “www-data“. Chown requires root permissions. If you need this line you will have to run phing as root.
Next target is inside build-configuration.xml. It fills configuration template with settings from *.properties files and save to config directory.
1 |
$ vim build/templates/application.ini.build |
1 2 3 4 5 6 7 8 9 10 |
db.host = "${db.host}" db.user = "${db.user}" db.password = "${db.password}" db.database = "${db.database}" log.level = "3" log.file = "logs/application.log" facebook.appId = "${facebook.appId}" facebook.secret = "${facebook.secret}" |
The last build file releases database changes. Dbdeploy task reads deltas from “database/delta“. Each delta should begin with a number, for example: 1-create-new-table.sql, 2-update-something.sql and so on. Those numbers are compared with local changelog stored in the target database (we will create this table in a second). Dbdeploy creates one patch file which consists of all missing deltas. The file will be saved as “database/deploy-db-${DSTAMP}${TSTAMP}.sql“.
Once the file is created we use good old fashion mysql client to insert it into a database. Phing doesn’t have build-in task to call MySql but you can run any command with “exec” task.
In order to make the above code work you need the “changelog” table. You can create it manually but since we are in the build script business leave that with Phing.
1 |
$ vim build/templates/database.sql |
1 2 3 4 5 6 7 8 |
CREATE TABLE changelog ( change_number BIGINT NOT NULL, delta_set VARCHAR(10) NOT NULL, start_dt TIMESTAMP NOT NULL, complete_dt TIMESTAMP NULL, applied_by VARCHAR(100) NOT NULL, description VARCHAR(500) NOT NULL ); |
1 |
$ phing database-init |
That should create the table in your database. In case of any problems you can troubleshoot it with “-verbose” parameter.
1 |
$ phing database-init -verbose |
You should get an error because the table already exists.
We almost done. The last step is to create example deltas.
1 |
$ vim database/delta/1-settings.sql |
1 2 3 4 5 6 7 8 9 10 11 12 |
CREATE TABLE IF NOT EXISTS `settings` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(32) NOT NULL, `value` varchar(256) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; --//@UNDO DROP TABLE `settings`; --// |
1 |
$ vim database/delta/2-settings-data.sql |
1 2 3 4 5 6 7 8 9 10 |
INSERT INTO `settings` (`id`, `name`, `value`) VALUES (NULL, 'language', 'EN'), (NULL, 'currency', 'EUR'); --//@UNDO DELETE FROM `settings` WHERE `name` IN ('language', 'currency'); --// |
You have probably noticed that both SQL files have @UNDO section. It’s not mandatory but some versions of Phing might not work without it. Code from @UNDO will go to “database/undo-db-${DSTAMP}${TSTAMP}.sql” file. It should allow to roll back database release in case of an error.
There is much more you can do with Phing. It has many tasks and extensions. Go though the manual to find out about all its features. XML is not the best language for programming but it does the job. So long you remember to keep it tight you shouldn’t have any problems.
4 Comments
Petah
12/04/2013Nice article. Just 1 comment on you database delta naming. You should use a timestamp instead of an incremental number, so when working in teams, your new deltas won’t conflict.
Lukasz Kujawa
12/04/2013Very good point! We keep having conflicts from time to time but I didn’t give any thought to it. That makes perfect sense, thank you for that Petah.
Damiano
20/12/2013Thank you! I was looking forward to find a post like this!
Is there anyway to get the SQL undo file automatically built by phing. If I remember correctly Weploy can do it
Dmitry Pashkevich
25/03/2014You can tell Composer where to put binaries when fetching dependencies:
“config”: {
“vendor-dir”: “src/vendor”,
“bin-dir”: “bin”
}
That way you will run phing via
bin/phing
from the project directory.