Building and Deploying PHP apps with Phing

Post on 27-Aug-2014

1.240 views 0 download

Tags:

description

Slides of the talk that I gave during PHP Johannesburg 2014 https://joind.in/talk/view/10411 Manually creating builds and running deployments can be scary, tedious, error-prone, boring, stressful (check all that apply). What you need is a tool that helps automate the necessary steps to build, test, package and deploy your app. During this talk you will be introduced to the workings of Phing, it's rich set of out-of-the-box tasks and easy extensibility. Step by step, you will learn how to write a comprehensive deployment script. A number of demonstrations will cover testing, packaging, database migration, continuous integration, multi-server deployments and other real-world use cases.

Transcript of Building and Deploying PHP apps with Phing

Building and deploying PHP apps with

>< ><

Michiel Rook

PHP Johannesburg April 2014

About me

• Freelance PHP & Java contractor / consultant

• PHP since '99

• Phing project lead

• Dutch Web Alliance

• http://www.linkedin.com/in/michieltcs

• @michieltcs

This talk

• Why a build tool

• What is Phing

• Usage

• Various examples

• Deployments

• Extending Phing

Why a build tool?

This is PHP!?

Repetitive tasks

• Version control

• Database changes

• Testing

• Minifying

• Packaging

• Uploading

• Deploying

• Configuring

Good programmers are lazy

Good programmers automate repeatablethings

Automate!

• Easier handover

• Improves quality

• Reduces errors

• Saves time

• Consolidate scripts, reduce technical debt

What is Phing?

Phing is AWESOME

What is Phing?

• “PHing Is Not GNU make; it's a PHP project build systemor build tool based on Apache Ant.”

• XML build files

• Mostly cross-platform

• Integrates various popular (PHP) tools

• Lots. Of. Tasks.

"Phing is good glue"

The basics

Installing Phing

• PEAR installation

$ pear channel-discover pear.phing.info$ pear install [--alldeps] phing/phing

• Optionally, install the documentation package

$ pear install phing/phingdocs• Composer

• Phar package

Build file

• XML• Contains standard elements

• Task: performs a specific function (copy, git commit, etc.)• Target: collection of tasks, can optionally depend on othertargets

• Project: root node, contains multiple targets

Example build file

<project name="Example" default="world"><echo>Hi!</echo>

<target name="hello"><echo>Hello</echo>

</target>

<target name="world" depends="hello"><echo>World!</echo>

</target></project>

Example build file

$ phing [-f build.xml]Buildfile: /home/michiel/phing/build.xml

[echo] Hi!

Example > hello:

[echo] Hello

Example > world:

[echo] World!

BUILD FINISHED

Properties

• Simple key-value pairs

• ${attribute}

• Replaced by actual value

Properties

<project name="Example" default="default"><target name="default">

<property file="build.properties" /><property name="foo" value="bar" />

<echo>${foo}</echo></target>

</project>

Properties

$ phingBuildfile: /home/michiel/phing/build.xml

Example > default:

[echo] bar

BUILD FINISHED

Fileset

• Denotes a group of files

• Include or exclude files based on patterns

• References: define once, use many

Fileset

<copy todir="build"><fileset dir="./application">

<include name="**/*.php" /><exclude name="**/*Test.php" />

</fileset></copy>

<fileset dir="./application" includes="**"/>

<fileset dir="./application" includes="**" id="files"/>

<fileset refid="files"/>

Fileset

• Selectors allow fine-grained matching on certain attributes

• contains, date, file name & size, ...

<fileset dir="${dist}"><and>

<filename name="**"/><date datetime="01/01/2011" when="before"/>

</and></fileset>

Conditions

• Nested elements that evaluate to booleans

• Used in "condition", "if" and "waitfor" tasks

Conditions

<if><equals arg1="${foo}" arg2="bar" /><then>

<echo message="The value of property foo is bar" /></then><else>

<echo message="The value of property foo is not bar" /></else>

</if>

Conditions

<if><available file="composer.json" /><then>

<exec checkreturn="true" command="composer install"passthru="true" logoutput="true" dir="build" />

</then></if>

Examples

Examples

• Version control

• Unit testing

• Packaging

• Deployment

• Database migration

• Continuous integration

Version control

• Git

• SVN

• CVS

Version control

<svnexportrepositoryurl="svn://localhost/project/trunk/"todir="/home/michiel/dev"/>

<svnlastrevisionrepositoryurl="svn://localhost/project/trunk/"propertyname="lastrev"/>

<echo>Last revision: ${lastrev}</echo>

Version control

<gitcommitrepository="/home/michiel/dev/phing"message="Update documentation" allFiles="true"/>

<gitpushrepository="/home/michiel/dev/phing"refspec="master" tags="true" />

PHPUnit

• Built-in support for most configuration options

• Gathers code coverage information

• Various output formats / reports

• PHPUnit 4.x support soon!

PHPUnit

• Stop the build when a test fails

<phpunit haltonfailure="true" haltonerror="true"bootstrap="my_bootstrap.php" printsummary="true"><batchtest>

<fileset dir="src"><include name="**/*Test.php"/>

</fileset></batchtest>

</phpunit>

Buildfile: /home/michiel/phpunit/build.xml

Demo > test:

[phpunit] Total tests run: 1, Failures: 1, Errors: 0,Incomplete: 0, Skipped: 0, Time elapsed: 0.00591 s

Execution of target "test" failed for the following reason:/home/michiel/phpunit/build.xml:3:44: Test FAILURE (testSayHello inclass HelloWorldTest): Failed asserting that two strings are equal.

PHPUnit example

• Determine which files to include in the coverage report

<coverage-setup database="reports/coverage.db"><fileset dir="src">

<include name="**/*.php"/><exclude name="**/*Test.php"/>

</fileset></coverage-setup>

• Gather code coverage and other data during the test run<phpunit codecoverage="true">

<formatter type="xml" todir="reports"/><batchtest>

<fileset dir="src"><include name="**/*Test.php"/>

</fileset></batchtest>

</phpunit>

PHPUnit example

• Generate some reports

<phpunitreport infile="reports/testsuites.xml"format="frames" todir="reports/tests"/>

<coverage-report outfile="reports/coverage.xml"><report todir="reports/coverage" title="Demo"/>

</coverage-report>

Documentation

• Phing currently integrates with popular documentationtools

• phpDocumentor (2)• ApiGen

• Also supports r(e)ST (reStructuredText)

<phpdoc2 title="Phing API Documentation" output="docs"><fileset dir="../../classes">

<include name="**/*.php"/></fileset>

</phpdoc2>

phpDocumentor

Packaging

• Create bundles or packages• tar• zip• phar• PEAR

Tar / zip

<tar compression="gzip" destFile="package.tgz"basedir="build"/>

<zip destfile="htmlfiles.zip"><fileset dir=".">

<include name="**/*.html"/></fileset>

</zip>

Phar packages

<pharpackagecompression="gzip"destfile="test.phar"stub="stub.php"basedir="."><fileset dir="hello">

<include name="**/**" /></fileset><metadata>

<element name="version" value="1.0" /><element name="authors">

<element name="John Doe"><element name="e-mail"

value="john@example.com" /></element>

</element></metadata>

</pharpackage>

SSH

<ssh username="john" password="smith"host="webserver" command="ls" />

<scp username="john" password="smith"host="webserver" todir="/www/htdocs/project/"><fileset dir="test">

<include name="*.html"/></fileset>

</scp>

Jenkins

Jenkins

Jenkins

Putting it all together

Build & deploy script

Objectives:

• Perform syntax check

• Run tests

• Create package

• Deploy via SSH

• To selectable target / environment

• Update database

• Roll back

Syntax checks & tests

<phplint haltonfailure="true"><fileset dir=".">

<include name="src/**" /></fileset>

</phplint>

<phpunit haltonfailure="true"><batchtest>

<fileset dir="."><include name="src/**/*Test.php" />

</fileset></batchtest>

</phpunit>

Packaging

<tstamp><format property="build.timestamp" pattern="%Y%m%d%H%M%S"/>

</tstamp>

<property name="build.release" value="${project.name}-${build.timestamp}" /><property name="package.name" value="${build.release}.tar.gz" />

<tar destfile="artifacts/${package.name}" basedir="${build.dir.project}" />

Multiple targets

• Several deployment targets: testing, staging, production, ...

• One property file per target

• Select based on input

ssh.host=127.0.0.1ssh.username=phingssh.key.private=development-sshssh.key.public=development-ssh.pubdeploy.location=/home/phing/apps

Multiple targets

<input propertyname="deploy.target"validArgs="testing,staging,production">

Enter target name</input>

<property file="${deploy.target}.properties"/>

Uploading

<ssh host="${ssh.host}"username="${ssh.username}"privkeyfile="${ssh.key.private}"pubkeyfile="${ssh.key.public}"command="mkdir -p ${deploy.location.project}/${build.release}"failonerror="true" />

<echo>Copying package</echo><scp host="${ssh.host}"

port="${ssh.port}"username="${ssh.username}"privkeyfile="${ssh.key.private}"pubkeyfile="${ssh.key.public}"todir="${deploy.location.project}/${build.release}"file="${package.name.full}" />

Uploading

<echo>Extracting package</echo><ssh ...

command="cd ${deploy.location.project}/${build.release};tar xzf ${package.name}"

failonerror="true" />

Symbolic links

• All releases stored in separate directories

• Symlink "current" to latest release

• Allows for easy (code) rollbacks

<echo>Creating symbolic link</echo><ssh ...

command="cd ${deploy.location.project};if [ -h &quot;current&quot; ]; thenrm -f previous; mv current previous; fi;ln -s ${build.release} current" />

Database migration

• Set of delta SQL files (1-create-post.sql)

• Tracks current version of your db in changelog table

• Generates do and undo SQL files

CREATE TABLE changelog (change_number BIGINT NOT NULL,delta_set VARCHAR(10) NOT NULL,start_dt TIMESTAMP NOT NULL,complete_dt TIMESTAMP NULL,applied_by VARCHAR(100) NOT NULL,description VARCHAR(500) NOT NULL

)

Database migration

• Delta scripts with do (up) & undo (down) parts

--//

CREATE TABLE `post` (`title` VARCHAR(255),`time_created` DATETIME,`content` MEDIUMTEXT

);

--//@UNDO

DROP TABLE `post`;

--//

Database migration

<dbdeployurl="sqlite:test.db"dir="deltas"outputfile="deploy.sql"undooutputfile="undo.sql"/>

<pdosqlexecsrc="deploy.sql"url="sqlite:test.db"/>

[dbdeploy] Getting applied changed numbers from DB:mysql:host=localhost;dbname=demo

[dbdeploy] Current db revision: 0[dbdeploy] Checkall:[pdosqlexec] Executing file: /home/michiel/dbdeploy/deploy.sql[pdosqlexec] 3 of 3 SQL statements executed successfully

Database migration

-- Fragment begins: 1 --INSERT INTO changelog

(change_number, delta_set, start_dt, applied_by, description)VALUES (1, 'Main', NOW(), 'dbdeploy','1-create_initial_schema.sql');

--//

CREATE TABLE `post` (`title` VARCHAR(255),`time_created` DATETIME,`content` MEDIUMTEXT

);

UPDATE changelogSET complete_dt = NOW()WHERE change_number = 1AND delta_set = 'Main';

-- Fragment ends: 1 --

Database migration

-- Fragment begins: 1 --

DROP TABLE `post`;

--//

DELETE FROM changelogWHERE change_number = 1AND delta_set = 'Main';

-- Fragment ends: 1 --

Restart services

<ssh ...command="sudo service apache2 reload"failonerror="true" />

Rolling back

<trycatch><try>

<ssh ...command="cd ${deploy.location.project};

rm -f current;mv -f previous current"

failonerror="true" /><echo>Rollback complete</echo>

</try><catch>

<echo>No previous version deployed!</echo></catch>

</trycatch>

Extending Phing

Writing your own task

• Extend from Task

• Contains main() method and optionally init()

• Setter method for each attribute in the build file

Our new task should

• Accept filesets

• Count number of lines in each file

• Fail the build if a file with zero lines is found

Our new task

<?php

require_once 'phing/Task.php';

class CountLinesTask extends Task{

public function main(){

$foundEmpty = false;

if ($foundEmpty) {throw new BuildException("One or more files have zero lines");

}}

}

Injecting file sets

private $_filesets = array();

/*** Creator for _filesets** @return FileSet*/public function createFileset(){

$num = array_push($this->_filesets, new FileSet());return $this->_filesets[$num-1];

}

Injecting file sets

foreach ($this->_filesets as $fileset) {$files = $fileset->getDirectoryScanner($this->project)

->getIncludedFiles();$dir = $fileset->getDir($this->project)->getAbsolutePath();

foreach ($files as $file) {$path = realpath($dir . DIRECTORY_SEPARATOR . $file);$lines = count(file($path));

$this->log($path . ": " . $lines . " line(s)");

if ($lines == 0) {$foundEmpty = true;

}}

}

Using the task

<project name="Count Lines" default="count"><target name="count">

<taskdef name="countlines"classpath="/home/michiel/tasks"classname="CountLinesTask" />

<countlines><fileset dir=".">

<include name="**/*.txt" /></fileset>

</countlines></target>

</project>

Using the task

Buildfile: /home/michiel/examples/count.xml

Count Lines > count:

[countlines] /home/michiel/examples/empty.txt: 0 line(s)[countlines] /home/michiel/examples/lines.txt: 3 line(s)Execution of target "count" failed for the following reason:/home/michiel/examples/count.xml:7:20: One or more files have zero lines

BUILD FAILED/home/michiel/examples/count.xml:7:20: One or more files have zero linesTotal time: 0.0454 seconds

Alternative: Ad Hoc

<target name="main"><adhoc-task name="foo"><![CDATA[class FooTask extends Task {

private $bar;

public function setBar($bar) {$this->bar = $bar;

}

public function main() {$this->log("In main(): " . $this->bar);

}}]]></adhoc-task><foo bar="TEST"/>

</target>

Where to go from here

• Tool versions

• Performance

• Documentation

• PHP 5.3/5.4/5.5

• IDE support

• CI integration

Questions?

Example code at https://github.com/mrook/phing-examplesPlease leave feedback at https://joind.in/10411

Contact us on:

http://www.phing.info#phing (freenode)@phingofficial

Thank you!