Phing for power users

Stephan Hochdörfer // 20.11.2013

About me

  • Stephan Hochdörfer
  • Head of IT, bitExpert AG (Mannheim, Germany)
  • S.Hochdoerfer@bitExpert.de
  • @shochdoerfer

  • #PHP, #DevOps, #Automation

What is Phing?





It is a PHP project build system or
build tool based on Apache Ant.

Domain-specific language

A glue for 3rd party tools

How to install Phing?

$> pear channel-discover pear.phing.info
$> pear install phing/phing
$> phing -v
Phing 2.5.0

Installing Phing globally?





Phing is "just another"
dependency for your project.

Installing Phing via Composer

{
	"require": {
		"phing/phing": "2.5.0"
	}
}
$> php composer.phar install
Loading composer repositories with package information
Installing dependencies
  - Installing phing/phing (2.5.0)
    Downloading: 100%         

Writing lock file
Generating autoload files

Installing Phing via Composer

/tmp/myproject
   |-vendor
   |---bin
   |-----@phing
   |---composer
   |---phing
   |-----phing
   |-------bin
   |---------phing
   |-------build
   |-------classes
   |-------docs
   |-------etc
   |-------test
$> ./vendor/bin/phing -v
Phing 2.5.0

Executing Phing

PHING_PATH=vendor/bin:bin
function phing {
    TEST_PATHS=($PHING_PATH)
    for TEST_PATH in "${TEST_PATHS[@]}"
    do
        if [ -x "${TEST_PATH}/phing" ]
        then
            "${TEST_PATH}/phing" "$@"
            return $?
        fi
    done

    PHING_ON_PATH="$(type -P phing )"
    if [ -n "${PHING_ON_PATH}" ]; then
        "${PHING_ON_PATH}" "$@"
        return $?
    else
        echo "phing not found!"
        return 127
    fi
}

Executing Phing

$> phing -v
Phing 2.5.0

Phing Basics



  • Project
  • Target
  • Task
  • Properties

Phing Basics: Project





Root node of a build file
containing one or more targets.

Phing Basics: Target





A group of tasks that
run as an entity.

Phing Basics: Task





Custom piece of code to
perform a specific function.

Phing Basics: Properties





Properties (variables) help
to customize execution.

Phing Basics: Properties





A lot of built-in properties e.g. host.os,
line.separator, phing.version, php.version, ...

A sample build file

<?xml version="1.0"?>
<project name="myproject" default="init">
	
	<target name="init">
		
		<!-- insert logic here -->
	</target>
</project>

Hello World example

<?xml version="1.0"?>
<project name="myproject" default="hello">
	
	<target name="hello" 
		description="Says Hello, world!">

		 <echo msg="Hello, world!" />
	</target>
</project>

Why Phing?


  • Runs everywhere where
    PHP runs
  • No additional depencendies
    needed (e.g. Java, ...)
  • More than 120 predefined
    tasks to choose from
  • Easy to extend by writing
    custom tasks in PHP

Enforce Internal Targets

<?xml version="1.0"?>
<project name="myproject" default="hello">

    <target name="init" description="Property initialization">
	<property name="Hello" value="Hello, world!" />
    </target>

    <target name="hello" depends="init">
	<echo msg="${Hello}" />
    </target>
</project>

Enforce Internal Targets

$> ./vendor/bin/phing -f build.xml hello
/tmp/myproject/build.xml

myproject > init:


myproject > hello:

     [echo] Hello, world!

BUILD FINISHED

Total time: 0.0474 seconds

Enforce Internal Targets

$> ./vendor/bin/phing -f build.xml init
/tmp/myproject/build.xml

myproject > init:


BUILD FINISHED

Total time: 0.0476 seconds

Enforce Internal Targets

$> ./vendor/bin/phing -l
Buildfile: /tmp/myproject/build.xml
Default target:
---------------------------------------------------------
 hello

Main targets:
---------------------------------------------------------
 init   Property initialization

Subtargets:
---------------------------------------------------------
 hello

Enforce Internal Targets





Internal targets are just helpers
like private methods.

Enforce Internal Targets

<?xml version="1.0"?>
<project name="myproject" default="hello">

    <target name="-init" description="Property initialization">
	<property name="Hello" value="Hello, world!" />
    </target>

    <target name="hello" depends="-init">
	<echo msg="${Hello}" />
    </target>
</project>

Enforce Internal Targets

$> ./vendor/bin/phing -f build.xml -init
Unknown argument: -init
phing [options] [target [target2 [target3] ...]]
Options:
  -h -help               print this message
  -l -list               list available targets
  -v -version            print the version information
  -q -quiet              be extra quiet
  -verbose               be extra verbose
  -debug                 print debugging information

Report bugs to <dev@phing.tigris.org>

Enforce Internal Targets





Are the targets really hidden?

Enforce Internal Targets

$> ./vendor/bin/phing -l
Buildfile: /tmp/myproject/build.xml
Default target:
---------------------------------------------------------
 hello

Main targets:
---------------------------------------------------------
 -init   Property initialization

Subtargets:
---------------------------------------------------------
 hello

Enforce Internal Targets

<?xml version="1.0"?>
<project name="myproject" default="hello">

    <target name="-init" description="Property initialization" 
        hidden="true">
	<property name="Hello" value="Hello, world!" />
    </target>

    <target name="hello" depends="-init">
	<echo msg="${Hello}" />
    </target>
</project>

Enforce Internal Targets

$> ./vendor/bin/phing -l
Buildfile: /tmp/myproject/build.xml
Default target:
---------------------------------------------------------
 hello

Subtargets:
---------------------------------------------------------
 hello

Custom tasks




Phing can do way more
than simple exec calls!

Custom task (Adhoc)

<?xml version="1.0"?>
<project name="myproject" default="hello">
    <target name="init"> 
	<adhoc-task name="mytask"><![CDATA[
	class MyTask extends Task {
		/**
		  * (non-PHPdoc)
		  * @see \Task::main()
		  */
		public function main() {
			// Custom code here...
		}
	}
	]]></adhoc-task>
    </target>

Custom task (Adhoc)

    <target name="hello" 
	depends="init">

	  <mytask />
    </target>
</project>

Custom task (External)

<?php
require_once 'phing/Task.php';

class MyTask extends Task {
	/**
	 * (non-PHPdoc)
	 * @see \Task::main()
	 */
	public function main() {
		// Custom code here...
	}
}

Custom task (External)

<?xml version="1.0"?>
<project name="myproject" default="hello">
    <target name="init"> 
	<taskdef
	    name="mytask"
	    classpath="${project.basedir}/src/"
	    classname="MyApp.Common.Phing.MyTask" />
    </target>

    <target name="hello" 
	depends="init">

	  <mytask />
    </target>
</project>

Custom task with params

<?php
require_once 'phing/Task.php';

class MyTask extends Task {
    protected $file;

    /**
      * @param string $file
      */
    public function setFile($file) {
	$this->file = $file;
    }

    /**
      * @see \Task::main()
      */
    public function main() {
	// Custom code here...
    }
}

Custom task with params

<?xml version="1.0"?>
<project name="myproject" default="hello">
    <target name="init"> 
	<taskdef
	    name="mytask"
	    classpath="${project.basedir}/src/"
	    classname="MyApp.Common.Phing.MyTask" />
    </target>

    <target name="hello" 
	depends="init">

	<mytask file="myfile.txt" />
    </target>
</project>

Why custom tasks?



  • Keep your build files clean and
    simple
  • Sometimes it es easier to write
    logic in PHP than in verbose XML
  • Makes it easier to reuse the tasks
    in other projects

How to ship your tasks?




Turn your custom tasks into first-class citizens
of your code base: Ship them with your code!

How to ship your tasks?




Create a Composer package
of your "common tasks"!

Properties File




Use external properties to
customize your build behaviour.

Properties File

<?xml version="1.0"?>
<project name="myproject" default="hello">

    <target name="hello" 
	description="Says whatever you want to say">

	<property file="./build.properties" />

	<echo msg="${Hello}" />
    </target>
</project>
Hello=Hello, world!

Properties File

$> phing
Buildfile: /tmp/myproject/build.xml

myproject > hello:

 [property] Loading /tmp/myproject/build.properties
     [echo] Hello, world!

BUILD FINISHED

Total time: 0.0601 seconds

Properties File: Improvement




Externalize properties and
offer customization capabilities

Properties File: Improvement

<?xml version="1.0"?>
<project name="myproject" default="hello">

    <target name="hello" depends="init">
	<echo msg="${Hello}" />
    </target>

    <target name="init" depends="prop, local-prop">
	<!-- some more init logic -->
    </target>

    <target name="prop">
	<echo message="Loading default build.properties"/>

	<property file="build.properties" />
    </target>

Properties File: Improvement

    <target name="local-prop" if="local-prop.exists"
	depends="local-prop-check">

	<echo message="Loading custom properties!"/>
	<property file="local.properties" override="true"/>
    </target>

    <target name="local-prop-check">
	<available file="local.properties"
	    property="local-prop.exists" />
    </target>
</project>

Properties File: Improvement

$> phing
Buildfile: /tmp/myproject/build.xml

myproject > prop:
     [echo] Loading default build.properties
 [property] Loading /tmp/myproject/build.properties

myproject > local-prop-check:

myproject > local-prop:

myproject > init:

myproject > hello:
     [echo] Hello, world!

BUILD FINISHED

Total time: 0.1383 seconds

Properties File: Improvement

$> phing
Buildfile: /tmp/myproject/build.xml

myproject > prop:
     [echo] Loading default build.properties
 [property] Loading /tmp/myproject/build.properties

myproject > local-prop-check:

myproject > local-prop:
     [echo] Loading custom properties!
 [property] Loading /tmp/myproject/local.properties

myproject > init:

myproject > hello:
     [echo] Hello my world!

BUILD FINISHED

Total time: 0.0493 seconds

build.properties example

phpunit.path=vendor/bin/phpunit
phpunit.junit.log=build/logs/junit.xml
phpunit.coverage.clover=build/logs/clover.xml
phpunit.coverage.html=build/coverage

phpcs.path=vendor/bin/phpcs
phpcs.ignore=/Phing/
phpcs.log=build/logs/checkstyle.xml

sencha.senchaCmd=/user/local/lib/sencha/sencha

comass.path=/var/lib/gems/1.8/bin/compass

build.properties example





Use distinct naming conventions
for your properties.

Accessing App Config





Duplicating configuration code is a bad habit.

Accessing App Config

<?php
require_once 'phing/Task.php';

class ConfigMapperTask extends Task {
    /**
      * @see \Task::main()
      */
    public function main() {
	// will import $APP_CONF in local context
	require_once('src/bootstrap.php');

	$project = $this->project;
	$project->setProperty('db.host', $APP_CONF['db_host']);
	$project->setProperty('db.user', $APP_CONF['db_user']);
	$project->setProperty('db.password', $APP_CONF['db_passwd']);
	$project->setProperty('db.database',
	    $APP_CONF['db_database']);
    }
}

Accessing App Config

<?xml version="1.0"?>
<project name="myproject" default="hello">

    <taskdef name="readAppConfig"
	classpath="${phing.dir}/src/"
	classname="MyApp.Common.Phing.AppConfigTask" />

    <target name="init" depends="prop, local-prop">
	<readAppConfig />
    </target>

    <target name="prop">
	<echo message="Load default build.properties"/>
	<property file="build.properties" />
    </target>

    <target name="local-prop" if="local-prop.exists"
	depends="local-prop-check">
    
    <!-- […] -->
</project>

Import Targets

<?xml version="1.0"?>
<project name="myproject" default="app:run">

    <!--
    The following target namespaces exist:
    db:*  - Database specific targets
    app:* - Application specific tasks
    ci:*  - CI server specific tasks
    -->
    <import file="build/build.db.xml" />
    <import file="build/build.app.xml" />
    <import file="build/build.ci.xml" />
</project>

Import Targets (Composer)

<?xml version="1.0"?>
<project name="myproject" default="app:run">

    <!--
    The following target namespaces exist:
    lib1:*  - Targets imported from lib1
    lib2:*  - Targets imported from lib2
    app:*   - Local application targets
    -->
    <import file="vendor/vendor1/lib1/build/build.xml" />
    <import file="vendor/vendor2/lib2/build/build.xml" />
    <import file="build/build.app.xml" />
</project>

Import Targets: Path handling





Be aware that imports behave
like include in PHP!

Import Targets: Path handling

<?xml version="1.0"?>
<project name="myproject" default="lib1:run">

    <!--
    The following target namespaces exist:
    lib1:*  - Targets imported from lib1
    -->
    <import file="vendor/lib1/build/build.xml" />
</project>
<?xml version="1.0"?>
<project name="lib1" default="lib1:run">

    <target name="lib1:run">

	<echo msg="Local dir: ${phing.dir.lib1}" />

	<echo msg="Global dir: ${phing.dir}" />
    </target>
</project>

Import Targets: Path handling

$> ./vendor/bin/phing
Buildfile: /tmp/myproject/build.xml

myproject > lib1:run:

     [echo] Local dir: /tmp/myproject/vendor/lib1/build
     [echo] Global dir: /tmp/myproject

BUILD FINISHED

Total time: 0.0411 seconds

Import Targets: Path handling





Be aware to always(!) use the
projects name in lowercase format!

Import Targets: Path handling





It`s ${phing.dir.myproject}
not ${phing.dir.MyProject}!

Distinct Target Naming

<?xml version="1.0"?>
<project name="myproject" default="ci:run-tests">

    <target name="app:clean-cache">
    </target>

    <target name="app:create-cache">
    </target>

    <target name="db:migrate">
    </target>

    <target name="js:minifiy">
    </target>

    <target name="ci:lint">
    </target>

    <target name="ci:run-tests">
    </target>
</project>

Use meaningful descriptions

<?xml version="1.0"?>
<project name="myproject" default="app:create-cache">

    <target name="app:clean-cache"
       description="Removes all cache files">
    </target>

    <target name="app:create-cache"
       description="Build the cache files for the xml configuration">
    </target>
</project>

Use meaningful descriptions

$> phing -l
Buildfile: /tmp/myproject/build.xml
Default target:
-----------------------------------------------------
 app:create-cache  Build the cache files for the xml
                   configuration

Main targets:
------------------------------------------------------
 app:clean-cache   Removes all cache files
 app:create-cache  Build the cache files for the xml
                   configuration

Prompt user for input

<?xml version="1.0"?>
<project name="myproject" default="run">

    <target name="run">
	<input propertyname="tag"
	      defaultValue="mytag">Tag to create?</input>

	<liquibase-tag
	      tag="${tag}"
	      jar="/opt/liquibase/liquibase.jar"
	      classpathref="/opt/liquibase/lib/mysql.jar"
	      changelogFile="${project.basedir}/diff.xml"
	      username="liquibase"
	      password="liquibase"
	      url="jdbc:mysql://localhost/myproject"/>
    </target>
</project>

Calling PHP functions

<?xml version="1.0"?>
<project name="myproject" default="run">

    <target name="run">

	<!-- 
	  Returns canonicalized absolute pathname 
	-->
	<php function="realpath" returnProperty="app.dir">
	    <param value="${app.dir}"/>
	</php>
    </target>
</project>

Restrict user access

<?xml version="1.0"?>
<project name="myproject" default="run">

    <target name="run">

	<!-- 
	  Check for root user
	-->
	<if>
	    <not>
			<equals arg1="${env.USER}" arg2="root" />
	    </not>
	    <then>
			<fail message="Wrong user!" />
	    </then>
	</if>
    </target>
</project>

Path handling

<?xml version="1.0"?>
<project name="myproject" default="ci:phpunit">

    <!-- 
	  ... 
    -->
    <target name="ci:phpunit" depends="-init, -ci:prepare">

	<resolvepath propertyName="phpunit.path.abs" 
	    dir="${phing.dir}"
	    file="${phpunit.path}"/>

	<exec executable="${phpunit.path.abs}" />
    </target>
</project>

Phing + Jenkins





Install the Jenkins Phing plugin

Phing + Jenkins: Config

Phing + Jenkins: Job Config

Phing + Jenkins: Job Config

Phing + Jenkins + Composer





Install the EnvInject plugin!

Phing + Jenkins + Composer

Phing + Jenkins + Composer

Phing + Jenkins + Composer

Follow conventions





Phing expects your build file to be called build.xml
and the builds properties file build.properties

Follow conventions





Pick meaningful, human-readable
names for targets and properties.

Follow conventions





Make build files self-contained.








Thank you!








Do not forget to rate the talk:
https://joind.in/10045