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 packagesVersionControl_SVN
and NET_FTP
. Both can be installed easily with the following commands:
$ sudo pear install VersionControl_SVN $ sudo pear install NET_FTPThe 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}/myappYou 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:
$ phingThe[-DPropertyName=PropertyValue]
-D
option allows you to redefine the value of a property declared with its override attribute set to “true”.
The ‘deploy’ Target
Thedeploy
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=myhostnameThe 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
Theprepare
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
Therelease
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
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 && sh install.sh [stage|local|prod] 2>&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.shThe 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 / ShutterstockFrequently 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 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.