Deploy and Release your Applications with Phing

Share this article

Suppose you have a web application that is installed on many hosts. Each installation may have a custom configuration, and the application is still in active development. You need an easy way to deploy new features and bug fixes to all of hosts. In his previous article Using Phing, the PHP Build Tool, Shameer gave you a basic understanding of Phing. You learned how to read and write a build file and what its basic components are: project, targets, tasks, and properties. In this article I show you how to use this knowledge to write a boilerplate build file, one that can be customized and reused in your real-world applications. We’ll use Phing’s Subversion tasks to manage the repository and the FileSync extension to syncronize your local installation with a remote server.

Preparing the Environment

In order to use the Subversion-related tasks, you need the two PEAR packages VersionControl_SVN and NET_FTP. Both can be installed easily with the following commands:
$ sudo pear install VersionControl_SVN
$ sudo pear install NET_FTP
The FileSyncTask extension is a friendly wrapper for the rsync command. It’s maintained by Federico Cargnelutti, and can be downloaded from his site. Once downloaded, copy the file FileSyncTask.php to Phing’s extensions directory (the default path on unix-like systems is /opt/phing/tasks/ext, but your configuration may be different). I’ve created an empty Subversion repository called “helloworld” and gave it a basic directory structure, with trunk, branches, and tags directories. I checked out the trunk locally and imported my application’s files using the following directory structure:

As you can see, there is a directory named build which will contain the files related to Phing. The basic build.xml file looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<project name="HelloWorld" default="hello" basedir="../">
 <!-- Load project settings from external file -->
 <property file="build/config/project.properties" />

 <!-- Default empty target -->
 <target name="hello" description="Displays basic project information">
  <echo message="Hello, welcome to ${phing.project.name}!" />
  <echo message="Current environment is: ${project.env}" />
 </target>
</project>
There’s a default target named “hello” that displays basic data, such as the project’s name and the current environment (production, development, staging, etc.). Instead of declaring a list of properties inside the build file though, Phing is told to load the project’s settings from an external file. The path of this file is relative to the basedir attribute defined in the property tag. By doing this, we can reuse the same file for multiple projects. The project.properties file is simply a text file which stores settings using a key-value syntax, for example:
ftp.host=host.example.com
ftp.port=21
ftp.username=example
ftp.password=123456
app.home=${ftp.host}/myapp
You can use the same ‘${varname}’ syntax to reference the value of a previously declared properties. Properties files are stored inside the build/config directory. Also in this directory is a hosts subdirectory, which contains specific client-host settings, and a scripts subdirectory that stores the install script template. Files ending in “-sample.properties” are versioned in the repository and used as templates, while the project-specific and host-specific files are marked as ignored. The build/export directory will contain the files generated by our targets. In order to run any of the targets we must call Phing from within the build directory. The common syntax is:
$ phing  [-DPropertyName=PropertyValue]
The -D option allows you to redefine the value of a property declared with its override attribute set to “true”.

The ‘deploy’ Target

The deploy target is our continuous integration tool. It syncronizes and deploys the current working copy with a remote host using a specific configuration (eg. servername.properties) stored in build/config/hosts. The target must be able to find the .properties file for the required host, and then connect to the remote host using those settings and perform the sync
task. The target is run with the following command:
$ phing deploy -Dhostname=myhostname
The target’s code is:
<target name="deploy" description="Deploys the current working copy to a remote host using FileSync">
 <!-- Default hostname is empty, must be passed from the command line -->
 <property name="hostname" value="false" override="true" />

 <!-- Set default LISTONLY to false -->
 <property name="listonly" value="false" override="true" />
 <property name="hostfile" value="build/config/hosts/${hostname}.properties" />

 <!-- Check for specific host/env file, if not fail! -->
 <available file="${hostfile}" property="hostfilefound" value="true"/>

 <fail unless="hostfilefound" message="Missing host configuration file (${hostfile})!" />

 <!-- Host file exists so loading... -->
 <property file="${hostfile}" />

 <!-- Get timestamp -->
 <tstamp />

 <!-- Set default VERBOSE flag to TRUE -->
 <if>
  <not>
   <isset property="sync.verbose" />
  </not>
  <then>
   <property name="sync.verbose" value="true" override="true" />
   <echo message="The value of sync.verbose has been set to true" />
  </then>
 </if>

 <!-- Set default DELETE flag to FALSE -->
 <if>
  <not>
   <isset property="sync.delete" />
  </not>
  <then>
   <property name="sync.delete" value="false" override="true" />
   <echo message="The value of sync.delete has been set to false" />
  </then>
 </if>

 <!-- Get auth info, password will be always required -->
 <property name="sync.remote.auth" value="${sync.remote.user}@${sync.remote.host}" />

 <!-- Perform Sync -->
 <!-- See: http://fedecarg.com/wiki/filesynctask -->
 <taskdef name="sync" classname="phing.tasks.ext.FileSyncTask" />
 <sync
  sourcedir="${sync.source.projectdir}" 
  destinationdir="${sync.remote.auth}:${sync.destination.projectdir}" 
  listonly="${listonly}" 
  excludefile="${sync.exclude.file}" 
  delete="${sync.delete}" 
  verbose="${sync.verbose}" />
</target>
First the hostname property is defined as overridable and assigned an arbitrary default value (I chose “true”). Then the listonly property is defined with a default value. This property is used by the sync task, and if it’s set to true then the task will only display a list of files that should be processed but will not perform the actual sync. The next three statements define the path of the host settings file and use the available task to set the hostfilefound property if the file is present. If it isn’t, the property won’t be set and the fail task will abort the script with the provided error message. If the host properties file is present then it’s loaded; this file contains a set of settings for the task grouped using the prefix “sync.”. They are all self-explanatory, but one worth paying special attention to is sync.delete: if set to true, all remote files that are not present in the local copy will be deleted. I recommend always setting it false unless you have a valid reason for it to be true. Next we can see two other examples of how powerful Phing is: we use the if/then syntax to define the value of the sync.delete and sync.verbose properties. The last part is the actual deploy task. The taskdef statement tells Phing that sync is a custom task and provides the path of the PHP file to load, and the parameters for this task are loaded from the host file. The excludefile attribute points to a text file containing a list of patterns to exclude from syncronization, one pattern per line, using rsync‘s syntax. Note: if your remote server use an SSH identity file you must set the identityfile attribute with the full path of your key file.

The ‘prepare’ Target

The prepare target’s job is to create a snapshot of the current trunk and tag it for release. The current trunk is copied under the tags/nameoftag directory of the repository (for example, tags/1.0.1). The code for prepare looks like:
<target name="prepare" description="Prepares a tag in the remote repository">

 <!-- Ask for a tag label to copy the current trunk -->
 <property name="tagLabel" value="false" override="true" />

 <!-- The tag name cannot be empty! -->
 <if>
  <isfalse value="${tagLabel}"/>
  <then>
   <fail message="Invalid tag name!" />
  </then>
 </if>

 <echo>Preparing tag ${tagLabel}...</echo>

 <!-- Copy trunk to the new tag under tags/tagLabel -->
 <svncopy
  force="true"
  nocache="true"
  repositoryurl="${svn.repository.url}/trunk"
  todir="${svn.repository.url}/tags/${tagLabel}"
  username="${svn.repository.username}"
  password="${svn.repository.password}"
  message="Tag release ${tagLabel}" />

 <!-- Switch the working copy repo to the newly created tag -->
 <svnswitch
  repositoryurl="${svn.repository.url}/tags/${tagLabel}"
  username="${svn.repository.username}"
  password="${svn.repository.password}"
  todir="." />

 <!-- Here you can perform any kind of editing: generate documentation, export SQL files, ecc -->
  <touch file="README.txt" />

 <!-- Commit changes -->
 <svncommit
  workingcopy="."
  message="Finish editing tag ${tagLabel}" />
 <echo message="Committed revision: ${svn.committedrevision}"/>

 <!-- Reset working copy repo to trunk -->
 <svnswitch
  repositoryurl="${svn.repository.url}/trunk" />

 <echo msg="Tag ${tagLabel} completed!" />
</target>
In the first couple lines we set a default value for the tag label because we want it to be passed from the command line, then we make sure that this value is not empty using the if/isFalse statement. Then, we use the svncopy task to copy our trunk to the desired tag directory and the svnswitch task to tell Subversion that we are now working on the newly created tag. From this point on we can make any modification to our files, such as updating a README
file or editing configuration files with suitable defaults. After all of the edits are performed, we use svncommit to save the changes and another svnswitch to reset our working copy back to the previous state.

The ‘release’ Target

The release target is our packaging tool. It starts by taking the trunk or the selected tag from the repository and performs the following actions with it:
  • export the tag files somewhere (by default to build/export)
  • create a TAR.GZ package from the exported files
  • compute the SHA1 digest of the compressed file
  • generate the installation script using the install.template.sh file
  • upload the files to the release server
The code for release is:
<target name="release" description="Exports the trunk or the given tag along with install scripts and FTP uploads">
 
 <property name="release" value="trunk" override="true" />
 
 <echo message="Creating package for '${release}'" />
 
 <!-- Process repository path for trunk or tag -->
 <if>
  <equals arg1="${release}" arg2="trunk" />
  <then>
   <property name="repo-path" value="${release}" override="true" />  
  </then>
  <else>
   <property name="repo-path" value="tags/${release}" override="true" /> 
  </else>
 </if>
 
 <!-- Export selected branch/tag from remote repository -->
 <svnexport 
  repositoryurl="${svn.repository.url}/${repo-path}"
  force="true"
  username="${svn.repository.username}"
  password="${svn.repository.password}"
  nocache="true"
  todir="${svn.export.basedir}/${release}" />
 
 <!-- Do other custom editing here... -->
 
 <!-- Create TAR archive -->
 <tar
  destfile="${svn.export.basedir}/${phing.project.name}-${release}.tar.gz"
  compression="gzip">
  <fileset dir="${svn.export.basedir}/${release}">
   <include name="*" />
  </fileset>
 </tar>
 
 <!-- Delete Temporary Export directory -->
 <delete
  dir="${svn.export.basedir}/${release}"
  includeemptydirs="true"
  verbose="false"
  failonerror="true" />

 <!-- Compute SHA1 digest -->
 <property name="hash" value="empty" />
 <filehash
  file="${svn.export.basedir}/${phing.project.name}-${release}.tar.gz"
  hashtype="1"
  propertyname="hash" />
 <echo msg="SHA1 Digest = ${hash}" />
 
 <echo msg="Files copied and compressed in build directory OK!" />
 
 <!-- Prepare install.sh, backup.sh and update.sh scripts -->
 <copy todir="${svn.export.basedir}" overwrite="true">
  <mapper type="glob" from="*.template.sh" to="*.sh"/>
  <fileset dir="./build/config/scripts">
   <include name="*.sh" />
  </fileset>
  <filterchain>
   <replacetokens begintoken="##" endtoken="##">
    <token key="SRCURL" value="${http.srcurl}/${release}/" />
    <token key="FILENAME" value="${phing.project.name}-${release}" />
    <token key="FILEXT" value="tar.gz" />
    <token key="HASH" value="${hash}" />
    <token key="APPNAME" value="${phing.project.name}" />
    <token key="APPVERSION" value="${release}" />
   </replacetokens>
  </filterchain>
 </copy>
 
 <!-- Upload the generated file(s) to FTP -->
 <property name="upload" value="false" override="true" />
 <if>
  <equals arg1="${upload}" arg2="true" />
  <then>
   <echo msg="Uploading to FTP server for release..." />
   <ftpdeploy 
    host="${ftp.host}" 
    port="${ftp.port}" 
    username="${ftp.username}" 
    password="${ftp.password}"
    dir="${ftp.dir}/${release}" 
    passive="${ftp.passive}"
    mode="${ftp.mode}">
    <fileset dir="${svn.export.basedir}">
     <include name="${phing.project.name}-${release}.tar.gz" />
     <include name="install.sh" />
    </fileset>
   </ftpdeploy>
 
   <echo>Now you can run: wget ${http.srcurl}/${release}/install.sh &amp;&amp; sh install.sh [stage|local|prod] 2>&amp;1 > ./install.log</echo>
  </then>
 </if>
 
 <echo msg="Done!" /> 
</target>
The first few lines deal with input parameters, with which we specify the default value “trunk” for the release to export and using the if statement to calculate the source repository path. The svnexport task exports the given files directly from the repository (not the working copy) into our destination directory. A temporary directory is created named “AppName-ReleaseName”. The tar task generates the package file from the temporary export directory using an internal fileset as source. The delete task, which is optional, deletes the temporary files. We then use the filehash task to generate a SHA1 digest for the packaged file (the other option is MD5) and store it in the hash property. The copy task is called to copy the template installer script into our export directory and uses some very powerful Phing resources. mapper is a filter selection and transformation tool applied to filenames. The files selected with the given fileset are processed by the mapper before the copy. In this case, the files matching “*.template.sh” are renamed with the extension “.sh” in the destination directory. Another powerful feature used here is filterchain. Phing filters are used to transform the content of files during the operation of another task. In this case the “parent” task is copy. The filter used here is replacetokens, which replaces in each file a set of defined template variables with a dynamically generated value. It’s used here to insert the specific release details (name, version, hash, url, etc.) into the install script. The final touch is the ftpdeploy task, triggered by the upload property. This task uploads our package file and the install script to the remote server and displays the URL to use for your installation. If there are no errors you can run the command:
$ wget http://www.yoursite.com/helloworld/releases/trunk/install.sh && sh install.sh
The commands download and verify the package file and then performs the installation tasks you specified.

Summary

We’ve seen a lot of useful Phing features here, but there are many more to discover. You can use the buildfile “as is” to deploy your current and future projects, or expand it by adding more features like support for unit testing, Git, database processing, and even custom extensions. As always, the official documentation will be your best place to start. Image via 1971yes / Shutterstock

Frequently Asked Questions about Deploying and Releasing Applications with Phing

What are the prerequisites for using Phing?

To use Phing, you need to have PHP installed on your system. Phing is a PHP project build system or build tool based on Apache Ant. You can use it to automate tasks such as running tests, creating documentation, or packaging a release. It’s a flexible, easy-to-use tool that can help you automate the process of building and deploying your PHP applications.

How do I install Phing?

Phing can be installed using Composer, a tool for dependency management in PHP. You can install Phing globally or locally in your project. To install it globally, use the command composer global require phing/phing. To install it locally, navigate to your project directory and use the command composer require --dev phing/phing.

How do I create a build file in Phing?

A build file in Phing is an XML file that defines what tasks should be performed and in what order. You can create a build file by creating a new XML file in your project directory. The root element of the file should be <project>, and each task is represented by an XML element within the project element.

How do I run a Phing build?

To run a Phing build, navigate to your project directory in the command line and use the command phing. This will run the build file named build.xml by default. If your build file has a different name, you can specify it by using the command phing -f buildfilename.xml.

Can I use Phing with continuous integration tools?

Yes, Phing can be used with continuous integration tools like Jenkins or Travis CI. You can configure these tools to run your Phing build as part of the build process, which can help automate the process of testing and deploying your application.

How do I define properties in Phing?

Properties in Phing are defined using the <property> element in your build file. You can set the name and value of the property using the name and value attributes, respectively. Properties can be used to store values that are used multiple times in your build file.

How do I handle errors in Phing?

Phing provides several ways to handle errors. You can use the fail task to stop the build if a certain condition is met. You can also use the trycatch task to catch exceptions and handle them in a specific way.

Can I extend Phing with custom tasks?

Yes, you can extend Phing with custom tasks. To create a custom task, you need to create a PHP class that extends the Task class provided by Phing. You can then use your custom task in your build file by using the taskdef task to define it.

How do I use Phing to run tests?

Phing can be used to run tests by using the phpunit task. This task runs PHPUnit tests and generates a report. You can specify the directory where your tests are located using the dir attribute.

How do I package a release with Phing?

You can use Phing to package a release by using the tar or zip tasks. These tasks create a tar or zip archive of your project. You can specify the files to include in the archive using filesets.

Vito TardiaVito Tardia
View Author

Vito Tardia (a.k.a. Ragman), is a web designer and full stack developer with 20+ years experience. He builds websites and applications in London, UK. Vito is also a skilled guitarist and music composer and enjoys writing music and jamming with local (hard) rock bands. In 2019 he started the BlueMelt instrumental guitar rock project.

Intermediate
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week