Continuous Integration with PHP

Author Note: This was originally posted on linux.com in 2008. I am replicating it here for posterity.

Large development projects can be difficult to manage. With multiple developers committing source code to one source tree, there are going to be times when code breaks and will not work. Running automated builds and tests on code can drastically reduce the time and effort developers spend fixing issues by catching them early. This process is called continuous integration. This article provides an overview of how to implement continuous integration in a PHP project using tools written in PHP.

Project tree and source control

Any development project worth doing needs some form of source control. Having a central location for developers to put source code is a must for continuous integration to work. Almost any type of source control software will do. For the purposes of this article we will use CVS (Concurrent Versioning System). If you need to learn how to set up a source control system, there are many fine resources on the Internet and in bookstores that explain the process.

To get started we will create a project called phpci and check it into CVS. The project will have three directories: src, test, and build. We will also have a few files in those directories. The src directory is where all the source code goes. build is where the build scripts reside. The test directory contains automated test scripts.

The phpci project contain one class, called Phpci (phpci/src/Phpci.php). In this class we will have a method that returns a simple formatted date string:

<?php
   class Phpci {
       public function getDate($format = "Y-m-d") {
           return date($format, time());
       }
   }
?>

Testing using PHPUnit

Once we have code, we’ll need to test it. Unit testing is an integral part of continuous integration; without automated testing the entire process would not make much sense. To test our code, we’ll use PHPUnit, a testing suite that is based on JUnit for Java. PHPUnit provides developers with a test harness that is automated, extensible, comprehensive, and easy to use.

A single unit test is a collection of test cases. Test cases must be designed to make sure that a piece of code operates in an expected manner no matter what is thrown its way.

PHPUnit unit tests are PHP classes that are extended from the PHPUnit2_Framework_TestCase class. By inheriting this class, almost everything that you need to write test cases will be available. All test cases in a test class must be public and begin with test. With just these few simple rules it is easy to create complete comprehensive unit test suites.

The following is a simple PHPUnit test for the Phpci class:

<?php
   require_once("../src/Phpci.php");
   require_once("PHPUnit2/Framework/TestCase.php");

   class TestCase_Phpci extends PHPUnit2_Framework_TestCase {
       private $phpci = null;

       protected function setUp() {
           $this->phpci = new Phpci();
       }

       protected function tearDown() {
           $this->phpci = null;
       }

       public function testGetUserRank() {
           $this->assertEquals(date("Y-m-d"), $this->phpci->getDate());
       }
   }
?>

Here we have one test case that uses the assertEquals method to make sure that when Phpci::getDate is called the return value is the current date in YYYY-MM-DD format. PHPUnit has many different assert methods that can be used in testing; see the documentation for a complete list.

You will also see that there are extra methods in the class — setUp and tearDown. These methods are known as fixtures. They allow you to set up the environment before any of the test cases are run. Fixture methods can be used to stage and clean a database, create and destroy objects, create and delete files, and many other things.

There are two primary ways to run PHPUnit tests. The first is on the command line:

$ phpunit [switches] UnitTest [UnitTest.php]

For our example we would use the command:

$ phpunit TestCase_Phpci 001_U_Phpci.php

This is a good way to run tests when you need to test one single thing, but it becomes bothersome when you need to run several tests. To run several tests, such as a suite in a continuous integration loop, Phing is a better tool.

Automating builds and tests with Phing

Phing (Phing Is Not GNU make) is an XML-based build system written in PHP and based on Apache Ant. The primary idea behind applications like Phing and Ant is to make building and packaging software easier for developers. By using Phing, you can automate almost all aspects of packaging, testing, and installing software written in PHP.

Phing uses XML build files to define processes called targets. Targets contain tasks that are defined to do specific operations, such as run PHPUnit test cases and install source code. By default Phing looks for a file called build.xml to run. Inside the build file Phing will run the default target if one is not specified.

To run the unit test that we created earlier we have a build file that looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." default="test" name="phpci">
   <target name="test" description="runs the test suite">
       <phpunit2 haltonfailure="true" haltonerror="true">
           <batchtest>
               <fileset dir="../test">
                   <include name="*_U_*.php"/>
               </fileset>
           </batchtest>
       </phpunit2>
   </target>
</project>

In the test target we have a task called phpunit2 that will run the test suite we have defined. Inside this task we have a batchtest task, and inside that a fileset task. This tells PHPUnit that we are going to run a series of tests. In the fileset task we define what files we want to use in our test suite. In our example we want to include all unit test files that fit a specific pattern. I use this feature to separate true unit tests from regression and interface tests. It does not have to be done this way — I just find it easier to divide tests so that specific functionality can be tested.

There is not much work involved in creating a Phing build file. From here it would be easy to add a target that moves the source code into a Web-accessible directory and set all of its configuration parameters. It would also be trivial to add a target that creates, configures, and populates a database. The ability to script operations like these makes Phing a useful tool for code migrations and making daily builds.

Automatically building and testing the code

Now that we have an automated build script and a testing suite, we can automate the build process and complete the continuous integration loop. Most projects that have a continuous integration loop in place use a dedicated workstation or server to run the builds. This is a good idea for many reasons. With a dedicated server you can emulate the production environment. The closer the build server’s configuration is to the production server’s, the more potential issues you can find.

In the Java world CruiseControl would be used to handle this process. It automates the process of running Ant scripts, stores all the results on a Web server, and reports the results to a list of users. There is one PHP tool that does this, Rephlux, but it is a new project and in it’s current state it is hard to use. Since there is no ready-made application, a simple shell script will work just fine.

The first item of business is to make a list of users that will be emailed with the results of the run:

EMAIL="cfiles@example.com"

Then we want to go to a directory somewhere so that we can get the source code and run the build script:

cd /var/www/localhost/htdocs

Next, we want to get a clean copy of the code. If this step fails we want to alert the users:

if ! cvs co phpci
   then
       echo "The CVS checkout for project phpci failed." | /bin/mail -s "[Build Notification] Project Phpci Failed" "$EMAIL"
       exit
fi

Now it’s time to test. We change into the build directory and run Phing. The results then get emailed to the users.

cd phpci/build

if /usr/bin/phing test
   then
       EMAILMESSAGE="The build for project phpci succeeded"
       EMAILSUBJECT="[Build Notification] Project Phpci Success"
   else
       EMAILMESSAGE="The build for project phpci failed"
       EMAILSUBJECT="[Build Notification] Project Phpci Failed"
fi

Finally, we want to clean up after the build:

cd ../../
rm -rf phpci

Of course you could do more here. If the build machine is running a Web server, you could copy the output from Phing and PHPUnit to the Web root on the server. Both pieces of software have flags that allow them to output formatted files.

The last thing that we want to do is run the shell script as a cron job. The job can be run as often or as little as the project dictates. Generally once a day, Monday through Friday, will do. My experience has been that midmorning works best; that gives you plenty of time to fix any errors, should they occur. To run the script Monday through Friday at 10:00 a.m., add the following line to the crontab file:

0 10 * * 1-5 /path/to/script

Conclusion

The simple examples here illustrate only a fraction of the functionality that each of these applications offers. We’ve only scratched the surface of what you can do with continuous integration, but I hope this article shows you that the concept is beneficial and easy to implement for your PHP project.