Symfony2 Continuous Integration with Jenkins, Ant, and Capistrano


At iostudio we work on many different projects. One of the ways we try to make sure that our quality of code is high is to use Jenkins to make sure all of our projects will build successfully and be able to deploy.

All of our new work gets is built using the Symfony2 framework. One advantage that we have found is that we are able to create our own iostudio Symfony Distribution that contains all of our build scripts and deployment scripts within. Now let’s get down to business and into some code.

The directory structure.

tree -L 1

If you are familiar with the Symfony2 Directory Structure You will notice a few things different. You see some new files (build.xml, Capfile) and directories (build/, config/).

The build.xml is an Ant build file that tells ant that it needs to clean up a few directories and get a few things ready before we start to run some build tests on the project. All the files related to this are kept in the build directory.

The Capfile and the config folder are the basis for our deployments using Capifony with a Multistage deployment.

The README.md file contains notes related to the project such as getting the project setup and running such as setting up the database and creating users. (Note: Some of our projects include a bash script that does all of this for you).

Before I go too much further I also want to stop for a second and talk about our Framework Bundle. We have our own customized framework bundle defined in composer.json that allows us to include cli commands to things such as create controllers, generate an admin backend, and check to see if the user coming to the site is using on a mobile platform or not.

Let’s pick apart the build.xml file now.

The build file is responsible for cleaning up after old builds, getting things ready for the new build, and running all the tests to make sure everything is good. We also put each task in it’s own file and have a task that we can use to updated those files to make sure everything stays in check.

First we need to clean up after old builds.

<!-- build/clean.xml -->
<project>
  <target name="clean"
          description="Cleanup build artifacts">
    <delete dir="${basedir}/build/api"/>
    <delete dir="${basedir}/build/code-browser"/>
    <delete dir="${basedir}/build/coverage"/>
    <delete dir="${basedir}/build/logs"/>
    <delete dir="${basedir}/build/pdepend"/>
    <antcall target="symfony.clean" />
  </target>
</project>

Now clean up symfony2 related stuff

<!-- build/symfony.clean.xml -->
<project>
  <target name="symfony.clean"
          description="Cleans up symfony data">
    <delete file="${basedir}/composer.phar" />
    <delete file="${basedir}/app/config/parameters.yml" />
    <delete dir="${basedir}/app/cache" />
  </target>
</project>

<!-- build/prepare.xml -->
<project>
  <target name="prepare"
          depends="clean"
          description="Prepare for build">
    <mkdir dir="${basedir}/build/api"/>
    <mkdir dir="${basedir}/build/code-browser"/>
    <mkdir dir="${basedir}/build/coverage"/>
    <mkdir dir="${basedir}/build/logs"/>
    <mkdir dir="${basedir}/build/pdepend"/>
    <antcall target="symfony.prepare" />
  </target>
</project>

<!-- build/symfony.prepare.xml -->
<project>
  <target name="symfony.prepare">
    <mkdir dir="${basedir}/app/cache" />
    <mkdir dir="${basedir}/web/uploads" />
    <copy file="${basedir}/app/config/parameters.yml.dist" tofile="${basedir}/app/config/parameters.yml" />
    <exec executable="bash">
      <arg value="-c" />
      <arg value="curl -s http://getcomposer.org/installer | php" />
    </exec>
    <exec executable="php">
      <arg value="composer.phar" />
      <arg value="install" />
    </exec>
    <exec executable="app/console">
      <arg value="doctrine:database:drop" />
      <arg value="-n" />
      <arg value="--force" />
    </exec>
    <exec executable="app/console">
      <arg value="doctrine:database:create" />
      <arg value="-n" />
    </exec>
    <exec executable="app/console">
      <arg value="doctrine:schema:update" />
      <arg value="-n" />
      <arg value="--complete" />
      <arg value="--force" />
    </exec>
    <exec executable="app/console">
      <arg value="assetic:dump" />
      <arg value="-n" />
      <arg value="--env=prod" />
      <arg value="--no-debug" />
    </exec>
    <exec executable="app/console">
      <arg value="assets:install" />
      <arg value="-n" />
      <arg value="--env=prod" />
      <arg value="--no-debug" />
      <arg value="web" />
    </exec>
  </target>
</project>

NOTE notice the line where it copies the parameters.yml.dist to parameters.yml. You database setting for the build server will be located in this file.

Now that we have everything built, let’s start with doing some lint checks on the php and twig files.

<!-- build/lint.xml -->
<project>
  <target name="lint"
          description="Syntax check">
    <apply executable="php" failonerror="true">
      <arg value="-l" />

      <fileset dir="${basedir}/app">
        <include name="**/*.php" />
        <exclude name="**/cache/**" />
      </fileset>

      <fileset dir="${basedir}/src">
        <include name="**/*.php" />
      </fileset>

      <fileset dir="${basedir}/web">
        <include name="**/*.php" />
      </fileset>
    </apply>

    <apply executable="app/console" failonerror="true">
      <arg value="twig:lint" />
      <arg value="--env=test" />

      <fileset dir="${basedir}/app/Resources">
        <include name="**/*.twig" />
      </fileset>

      <fileset dir="${basedir}/src">
        <include name="**/*.twig" />
      </fileset>
    </apply>
  </target>
</project>

phploc

<!-- build/phploc.sml -->
<project>
  <target name="phploc"
          description="Measure project size using PHPLOC">
    <exec executable="phploc">
      <arg value="--log-csv" />
      <arg value="${basedir}/build/logs/phploc.csv" />
      <arg value="--exclude" />
      <arg value="${basedir}/app" />
      <arg value="--exclude" />
      <arg value="${basedir}/web" />
      <arg path="${basedir}" />
    </exec>
  </target>
</project>

pdepend

<!-- build/pdepend.xml -->
<project>
  <target name="pdepend"
          description="Calculate software metrics using PHP_Depend">
    <exec executable="pdepend">
      <arg value="--jdepend-xml=${basedir}/build/logs/jdepend.xml" />
      <arg value="--jdepend-chart=${basedir}/build/pdepend/dependencies.svg" />
      <arg value="--overview-pyramid=${basedir}/build/pdepend/overview-pyramid.svg" />
      <arg value="--ignore=${basedir}/app,${basedir}/vendor" />
      <arg path="${basedir}" />
    </exec>
  </target>
</project>

phpcpd

<!-- build/phpcpd.xml -->
<project>
  <target name="phpcpd"
          description="Find duplicate code using PHPCPD">
    <exec executable="phpcpd">
      <arg value="--log-pmd" />
      <arg value="${basedir}/build/logs/pmd-cpd.xml" />
      <arg value="--exclude" />
      <arg value="${basedir}/app" />
      <arg value="--exclude" />
      <arg value="${basedir}/vendor" />
      <arg path="${basedir}" />
    </exec>
  </target>
</project>

phpcs

<!-- build/phpcs.xml -->
<project>
  <target name="phpcs"
          description="Find coding standard violations using PHP_CodeSniffer">
          <exec executable="phpcs" output="/dev/null">
      <arg value="--report=checkstyle" />
      <arg value="--report-file=${basedir}/build/logs/checkstyle.xml" />
      <arg value="--standard=${basedir}/build/ruleset/phpcs.xml" />
      <arg value="--ignore=${basedir}/vendor/*,${basedir}/app/*,${basedir}/web/*,*.js,*.css" />
      <arg path="${basedir}" />
    </exec>
  </target>
</project>

We use the psr standards here.

phpmd

<!-- build/phpmd.xml -->
<project>
  <target name="phpmd"
          description="Perform project mess detection using PHPMD">
    <exec executable="phpmd">
      <arg path="${basedir}/app/Resources,${basedir}/src" />
      <arg value="xml" />
      <arg value="${basedir}/build/ruleset/phpmd.xml" />
      <arg value="--reportfile" />
      <arg value="${basedir}/build/logs/pmd.xml" />
    </exec>
  </target>
</project>

phpunit

<!-- build/symfony.test-all.xml -->
<project>
  <target name="symfony.test-all"
          description="Runs all the functional and unit test">
    <exec executable="phpunit"
          failonerror="true">
      <arg value="-c" />
      <arg value="app/" />
    </exec>
  </target>
</project>

So there are most of the build scripts. Each are in it’s own file to allow us to run a quick script to update all of these. Now Jenkins needs to be setup. We have our own symfony2 jenkins template that we use that includes all the tools that we use to read the output of all the tests above. We also have some documentation auto generated for us using phpdoc. I have left this script out and you can insert your favorite php documentor for that.

There are various tutorials on setting up Jenkins so I will skip that and get into deploying with Capifony. The plugin that we use is the Post Build Task which only runs when everything else is within an acceptable range. ie None of the phpunit tests failed.

Let’s now go over the Capifony configs. For each deployment we only want to share the logs, the uploads, and the vendor directories. We don’t share the parameters.yml file.

The reason why the parameters.yml file is not shared is because for each project we maintain a parameters.yml.stage. Stage refers to the capistrano multistage extension. For every project we have at least a beta server and a production server. The configuration for this is setup in the specific files.

Allow me to rant for a moment on this. The reason why this are setup like this is so we can look at the parameters.yml.beta file and see how it’s setup. Most of the time we use a throw away Google Analytics account on the beta server to avoid beta tests getting into production analytic logs. It’s also to avoid having to ssh into the servers and see what is configured there.

Once the deployment takes place, we hook into the deploy:finalize_update task and do two things. First we have a robots.txt.beta that we copy over to the web folder that tells search engines to keep the beta site out of it’s indexes. The other is to copy over the parameters.yml.beta file.

# app/config/deploy.rb
# ... snip ...
before("deploy:finalize_update") do
  run "if [ -f #{current_release}/web/robots.txt.#{stage} ]; then cp #{current_release}/web/robots.txt.#{stage} #{current_release}/web/robots.txt; fi"
  run "if [ -f #{current_release}/app/config/parameters.yml.#{stage} ]; then cp #{current_release}/app/config/parameters.yml.#{stage} #{current_release}/app/config/parameters.yml; fi"
end
# ... snip ...

It takes awhile to get all of this setup for each of our clients, but we have found that deploying code reduces the amount of people asking for us to deploy code up for our QA department or for client approval.